@termwright/probe-tview 0.2.0 → 0.3.1
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 +99 -89
- package/assets/tcell_marker_windows.go.txt +80 -0
- package/assets/tview_probe.go.txt +1368 -0
- package/assets/tview_probe_test.go.txt +483 -0
- package/dist/index.d.ts +16 -32
- package/dist/index.js +118 -116
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
- package/upstream-patches/tview/v0.42.0/add/termwright_probe.go +0 -876
- package/upstream-patches/tview/v0.42.0/add/termwright_probe_test.go +0 -428
- package/upstream-patches/tview/v0.42.0/manifest.json +0 -42
- package/upstream-patches/tview/v0.42.0/patches/application.go.patch +0 -14
- package/upstream-patches/tview/v0.42.0/patches/go.mod.patch +0 -15
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
package tview
|
|
2
|
+
|
|
3
|
+
// Add-only instrumentation injected by Go's compiler tool executor. Upstream
|
|
4
|
+
// bytes are never copied or edited; this compilation unit joins package tview
|
|
5
|
+
// only for a Termwright-owned build.
|
|
6
|
+
//
|
|
7
|
+
// Being inside the package is necessary only for sealed state with no public
|
|
8
|
+
// accessor: Grid items/visibility, Modal content, disabled state that tview
|
|
9
|
+
// does not expose, sensitive-value transforms, and indexed DropDown options.
|
|
10
|
+
// Everything else below deliberately uses public getters.
|
|
11
|
+
//
|
|
12
|
+
// Three rules from the Phase 0 audit (docs/architecture/audit/tview.md §1–2)
|
|
13
|
+
// are load-bearing, and breaking any of them turns a working application into
|
|
14
|
+
// a hang or a lie:
|
|
15
|
+
//
|
|
16
|
+
// 1. Screen.Show runs inside Application.draw(), which holds the application's
|
|
17
|
+
// write lock for the whole frame. The decorator must never wait on or
|
|
18
|
+
// re-enter the event loop from that boundary.
|
|
19
|
+
// 2. Reading primitive state is safe at this boundary and essentially nowhere else:
|
|
20
|
+
// rects are assigned by parents *during* the draw, so another goroutine
|
|
21
|
+
// reading GetRect races the layout.
|
|
22
|
+
// 3. The marker must follow the frame's bytes on the screen's exact writer.
|
|
23
|
+
// Semantic socket delivery is independently ordered by a bounded worker;
|
|
24
|
+
// only complete pre-encoded revisions admitted to it receive a marker.
|
|
25
|
+
|
|
26
|
+
import (
|
|
27
|
+
"errors"
|
|
28
|
+
"reflect"
|
|
29
|
+
"runtime/debug"
|
|
30
|
+
"sort"
|
|
31
|
+
"strconv"
|
|
32
|
+
"strings"
|
|
33
|
+
"sync"
|
|
34
|
+
"sync/atomic"
|
|
35
|
+
|
|
36
|
+
"github.com/gdamore/tcell/v2"
|
|
37
|
+
|
|
38
|
+
"github.com/gorce-ai/termwright/clients/go/annotate"
|
|
39
|
+
"github.com/gorce-ai/termwright/clients/go/evidence"
|
|
40
|
+
"github.com/gorce-ai/termwright/clients/go/probehost"
|
|
41
|
+
"github.com/gorce-ai/termwright/clients/go/protocol"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// probeName and probeVersion identify this probe in the handshake, distinctly
|
|
45
|
+
// from the hand-written adapter so a session can be told apart in diagnostics.
|
|
46
|
+
const (
|
|
47
|
+
probeName = "termwright-probe-tview"
|
|
48
|
+
probeVersion = "0.3.1"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// termwrightProbeState is nil for an uninstrumented run, which is every run
|
|
52
|
+
// that does not carry the handshake variables.
|
|
53
|
+
type termwrightProbeState struct {
|
|
54
|
+
client *protocol.Client
|
|
55
|
+
publisher atomic.Pointer[protocol.PublicationQueue]
|
|
56
|
+
shutdown sync.Once
|
|
57
|
+
application *Application
|
|
58
|
+
recoveryStop chan struct{}
|
|
59
|
+
recoveryStopOnce sync.Once
|
|
60
|
+
recoveryPending atomic.Bool
|
|
61
|
+
recoveryWorkers sync.WaitGroup
|
|
62
|
+
|
|
63
|
+
mu sync.Mutex
|
|
64
|
+
ids map[Primitive]string
|
|
65
|
+
nextID int
|
|
66
|
+
dropped atomic.Uint64
|
|
67
|
+
timedOut atomic.Uint64
|
|
68
|
+
frames atomic.Uint64
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type termwrightPublicationSink interface {
|
|
72
|
+
TryPublish(*protocol.Snapshot) (string, error)
|
|
73
|
+
ReadyAfterDrop() <-chan struct{}
|
|
74
|
+
Fail(string, string)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// TermwrightProbeStats reports what the probe did and failed to do.
|
|
78
|
+
//
|
|
79
|
+
// Exported because the conformance fixture asserts on it: a drop counter no
|
|
80
|
+
// test can read is a drop counter nobody notices.
|
|
81
|
+
type TermwrightProbeStats struct {
|
|
82
|
+
Frames uint64
|
|
83
|
+
Dropped uint64
|
|
84
|
+
TimedOut uint64
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// TermwrightProbeStatistics returns the counters, or zeroes when dormant.
|
|
88
|
+
func TermwrightProbeStatistics() TermwrightProbeStats {
|
|
89
|
+
p := termwrightLastProbe.Load()
|
|
90
|
+
if p == nil {
|
|
91
|
+
return TermwrightProbeStats{}
|
|
92
|
+
}
|
|
93
|
+
return TermwrightProbeStats{
|
|
94
|
+
Frames: p.frames.Load(),
|
|
95
|
+
Dropped: p.dropped.Load(),
|
|
96
|
+
TimedOut: p.timedOut.Load(),
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
var (
|
|
101
|
+
termwrightLastProbe atomic.Pointer[termwrightProbeState]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// termwrightScreen turns the public Screen.Show boundary into the causal
|
|
105
|
+
// commit hook tview itself does not expose. Embedding preserves the complete
|
|
106
|
+
// tcell.Screen surface; only Show is observed. The underlying screen remains
|
|
107
|
+
// the sole owner of terminal output and marker delivery.
|
|
108
|
+
type termwrightScreen struct {
|
|
109
|
+
tcell.Screen
|
|
110
|
+
application *Application
|
|
111
|
+
commit func(Primitive, tcell.Screen)
|
|
112
|
+
fail func(string, string)
|
|
113
|
+
previousBefore func(tcell.Screen) bool
|
|
114
|
+
previousAfter func(tcell.Screen)
|
|
115
|
+
beforeHook func(tcell.Screen) bool
|
|
116
|
+
afterHook func(tcell.Screen)
|
|
117
|
+
showing atomic.Bool
|
|
118
|
+
failed atomic.Bool
|
|
119
|
+
detached atomic.Bool
|
|
120
|
+
cleaned atomic.Bool
|
|
121
|
+
finalized atomic.Bool
|
|
122
|
+
phase atomic.Int32
|
|
123
|
+
diagnose sync.Once
|
|
124
|
+
diagnosis sync.WaitGroup
|
|
125
|
+
owned bool
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const (
|
|
129
|
+
termwrightPhaseIdle int32 = iota
|
|
130
|
+
termwrightPhaseCleared
|
|
131
|
+
termwrightPhaseDrawing
|
|
132
|
+
termwrightPhaseFinal
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// Clear observes tview's public frame prelude without changing it. SetRoot may
|
|
136
|
+
// also clear between frames, so only an idle screen starts a new candidate;
|
|
137
|
+
// the before-draw hook remains the authoritative transition into drawing.
|
|
138
|
+
func (s *termwrightScreen) Clear() {
|
|
139
|
+
s.phase.CompareAndSwap(termwrightPhaseIdle, termwrightPhaseCleared)
|
|
140
|
+
s.Screen.Clear()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func (s *termwrightScreen) beforeDraw(screen tcell.Screen) bool {
|
|
144
|
+
if !s.phase.CompareAndSwap(termwrightPhaseCleared, termwrightPhaseDrawing) {
|
|
145
|
+
s.failClosed("tview entered beforeDraw without completing the previous decorated frame")
|
|
146
|
+
s.phase.Store(termwrightPhaseDrawing)
|
|
147
|
+
}
|
|
148
|
+
shortCircuit := false
|
|
149
|
+
if s.previousBefore != nil {
|
|
150
|
+
shortCircuit = s.previousBefore(screen)
|
|
151
|
+
}
|
|
152
|
+
if shortCircuit {
|
|
153
|
+
s.phase.CompareAndSwap(termwrightPhaseDrawing, termwrightPhaseFinal)
|
|
154
|
+
}
|
|
155
|
+
return shortCircuit
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func (s *termwrightScreen) afterDraw(screen tcell.Screen) {
|
|
159
|
+
// The application's callback may itself call Show. Keep that Show unarmed:
|
|
160
|
+
// only tview's own Show after this wrapper returns is the final boundary.
|
|
161
|
+
if s.previousAfter != nil {
|
|
162
|
+
s.previousAfter(screen)
|
|
163
|
+
}
|
|
164
|
+
if !s.phase.CompareAndSwap(termwrightPhaseDrawing, termwrightPhaseFinal) {
|
|
165
|
+
s.failClosed("tview entered afterDraw outside the decorated frame phase")
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func termwrightSameHook(left, right any) bool {
|
|
170
|
+
if left == nil || right == nil {
|
|
171
|
+
return left == nil && right == nil
|
|
172
|
+
}
|
|
173
|
+
return reflect.ValueOf(left).Pointer() == reflect.ValueOf(right).Pointer()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
func (s *termwrightScreen) hooksIntact() bool {
|
|
177
|
+
return termwrightSameHook(s.application.beforeDraw, s.beforeHook) &&
|
|
178
|
+
termwrightSameHook(s.application.afterDraw, s.afterHook)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func (s *termwrightScreen) failClosed(message string) {
|
|
182
|
+
if !s.failed.CompareAndSwap(false, true) {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
if s.fail != nil {
|
|
186
|
+
s.fail("adapter-guarantee-violation", message)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
func (s *termwrightScreen) Show() {
|
|
191
|
+
if !s.showing.CompareAndSwap(false, true) {
|
|
192
|
+
// A Screen implementation which re-enters its decorator cannot provide a
|
|
193
|
+
// single completed output boundary. Refuse semantics without taking a
|
|
194
|
+
// mutex (which would deadlock the application draw goroutine).
|
|
195
|
+
s.failClosed("tview screen re-entered Show before the previous frame committed")
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
defer s.showing.Store(false)
|
|
199
|
+
|
|
200
|
+
s.Screen.Show()
|
|
201
|
+
phase := s.phase.Load()
|
|
202
|
+
// Idle Show calls are outside Application.draw and carry no semantic frame.
|
|
203
|
+
// In particular, do not read the lock-free upstream hook fields there: an
|
|
204
|
+
// application may legally configure them before Run. Non-idle phases are
|
|
205
|
+
// entered only by Application.Clear/beforeDraw while draw holds its lock.
|
|
206
|
+
if phase != termwrightPhaseIdle && !s.hooksIntact() {
|
|
207
|
+
s.failClosed("tview replaced a decorated beforeDraw or afterDraw hook at runtime; final frame observation cannot continue")
|
|
208
|
+
}
|
|
209
|
+
if s.detached.Load() || s.failed.Load() {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
// A custom Primitive or application hook may call Show while drawing. Those
|
|
213
|
+
// output flushes are real, but they are not complete Application frames. The
|
|
214
|
+
// composed lifecycle hooks arm exactly the Show issued by Application.draw.
|
|
215
|
+
if phase != termwrightPhaseFinal || !s.phase.CompareAndSwap(termwrightPhaseFinal, termwrightPhaseIdle) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
// Application.draw holds the application write lock across Screen.Show.
|
|
219
|
+
// Direct access is therefore the lock-safe T1 equivalent of a public root
|
|
220
|
+
// getter and follows SetRoot changes frame by frame.
|
|
221
|
+
if s.commit != nil {
|
|
222
|
+
s.commit(s.application.root, s.Screen)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Fini distinguishes normal Application.Stop (which clears application.screen
|
|
227
|
+
// first) from a runtime SetScreen replacement (which finalizes the current
|
|
228
|
+
// screen while it is still installed). Replacements cannot silently continue
|
|
229
|
+
// with an undecorated sink: semantic publication fails closed before the new
|
|
230
|
+
// screen can render.
|
|
231
|
+
func (s *termwrightScreen) Fini() {
|
|
232
|
+
// Stop calls Screen.Fini while holding Application's write lock. Waiting for
|
|
233
|
+
// an RLock here would therefore deadlock the application. SetScreen, in
|
|
234
|
+
// contrast, releases the lock before finalizing the old screen, so a
|
|
235
|
+
// successful TryRLock lets us diagnose that unsupported replacement exactly.
|
|
236
|
+
replaced := false
|
|
237
|
+
if s.application.TryRLock() {
|
|
238
|
+
replaced = s.application.screen == s
|
|
239
|
+
s.application.RUnlock()
|
|
240
|
+
} else {
|
|
241
|
+
// The normal case is Stop. Defer the distinction until the writer releases
|
|
242
|
+
// the lock: Stop has already cleared screen, whereas an unrelated writer
|
|
243
|
+
// leaves a live Application whose semantics must fail with a diagnostic.
|
|
244
|
+
// Add to the lifecycle group before publishing detached=true so cleanup
|
|
245
|
+
// can never race Wait against Add.
|
|
246
|
+
s.diagnose.Do(func() {
|
|
247
|
+
s.diagnosis.Add(1)
|
|
248
|
+
s.failed.Store(true)
|
|
249
|
+
s.detached.Store(true)
|
|
250
|
+
go func() {
|
|
251
|
+
defer s.diagnosis.Done()
|
|
252
|
+
s.application.RLock()
|
|
253
|
+
alive := s.application.screen != nil
|
|
254
|
+
s.application.RUnlock()
|
|
255
|
+
if alive && s.fail != nil {
|
|
256
|
+
s.fail("adapter-guarantee-violation", "tview screen finalized while the Application remained active; semantic observation cannot continue")
|
|
257
|
+
}
|
|
258
|
+
}()
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
if replaced {
|
|
262
|
+
s.failed.Store(true)
|
|
263
|
+
s.detached.Store(true)
|
|
264
|
+
if s.fail != nil {
|
|
265
|
+
s.fail("adapter-guarantee-violation", "tview replaced its decorated Screen at runtime; the new output sink is not semantically observed")
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
s.finishUnderlying()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
func (s *termwrightScreen) finishUnderlying() {
|
|
272
|
+
if s.finalized.CompareAndSwap(false, true) {
|
|
273
|
+
s.Screen.Fini()
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
func init() {
|
|
278
|
+
probehost.Register("tview", func(application, root any) (func(), error) {
|
|
279
|
+
a, applicationOK := application.(*Application)
|
|
280
|
+
primitive, rootOK := root.(Primitive)
|
|
281
|
+
if !applicationOK || a == nil || !rootOK || primitive == nil {
|
|
282
|
+
return nil, errors.New("termwright tview probe requires a non-nil *tview.Application and tview.Primitive root")
|
|
283
|
+
}
|
|
284
|
+
return termwrightAttach(a, primitive)
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// newTermwrightProbe honours the dormant rule: without an endpoint and a token
|
|
289
|
+
// the compiler-injected build behaves exactly like upstream. `FromEnv` returns nil in that case,
|
|
290
|
+
// so there is one branch and no second source of truth about what "dormant"
|
|
291
|
+
// means.
|
|
292
|
+
func newTermwrightProbe(frameworkVersion string) *termwrightProbeState {
|
|
293
|
+
client := protocol.FromEnv(protocol.Options{
|
|
294
|
+
AdapterName: probeName,
|
|
295
|
+
AdapterVersion: probeVersion,
|
|
296
|
+
Probe: &protocol.ProbeInfo{
|
|
297
|
+
Framework: "tview",
|
|
298
|
+
FrameworkVersion: frameworkVersion,
|
|
299
|
+
ProbeVersion: probeVersion,
|
|
300
|
+
IdentityKind: protocol.ProbeIdentityStable,
|
|
301
|
+
Capabilities: []protocol.ProbeCapability{
|
|
302
|
+
protocol.ProbeCapStableIdentity,
|
|
303
|
+
protocol.ProbeCapIntendedRect,
|
|
304
|
+
protocol.ProbeCapAnnotations,
|
|
305
|
+
},
|
|
306
|
+
Instrumentation: &protocol.ProbeInstrumentation{
|
|
307
|
+
HighestTier: protocol.ProbeTierT1,
|
|
308
|
+
SemanticClass: protocol.ProbeSemanticClassA,
|
|
309
|
+
DegradedCapabilities: []protocol.SessionCapabilityID{
|
|
310
|
+
"clipped-geometry", "custom-container-enumeration",
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
Capabilities: []protocol.Capability{
|
|
315
|
+
protocol.CapTree,
|
|
316
|
+
protocol.CapIntendedGeometry,
|
|
317
|
+
protocol.CapStates,
|
|
318
|
+
protocol.CapFocusState,
|
|
319
|
+
protocol.CapActions,
|
|
320
|
+
protocol.CapActionRecipes,
|
|
321
|
+
protocol.CapRenderRevisions,
|
|
322
|
+
},
|
|
323
|
+
// Socket deadlines are worker watchdogs only. The render hook never
|
|
324
|
+
// performs a socket write; it uses bounded non-blocking admission.
|
|
325
|
+
WriteTimeout: protocol.DefaultWriteTimeout,
|
|
326
|
+
// Freeze the application's production evidence providers into the same
|
|
327
|
+
// hello as the certified tview adapter. Providers only contribute facts;
|
|
328
|
+
// all actions still enter through tcell's real terminal input path.
|
|
329
|
+
EvidenceProviders: evidence.DefaultRegistry(),
|
|
330
|
+
})
|
|
331
|
+
if client == nil {
|
|
332
|
+
return nil
|
|
333
|
+
}
|
|
334
|
+
return &termwrightProbeState{
|
|
335
|
+
client: client,
|
|
336
|
+
ids: make(map[Primitive]string),
|
|
337
|
+
recoveryStop: make(chan struct{}),
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
func (p *termwrightProbeState) connect() error {
|
|
342
|
+
if err := p.client.Start(protocol.DialTimeout); err != nil {
|
|
343
|
+
return err
|
|
344
|
+
}
|
|
345
|
+
publisher, err := protocol.NewPublicationQueue(p.client, 2)
|
|
346
|
+
if err != nil {
|
|
347
|
+
return err
|
|
348
|
+
}
|
|
349
|
+
p.publisher.Store(publisher)
|
|
350
|
+
return nil
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// termwrightAttach is called by the public one-line adapter before Run. The
|
|
354
|
+
// handshake, screen creation and allocations therefore happen outside
|
|
355
|
+
// Application.draw's lock.
|
|
356
|
+
func termwrightAttach(a *Application, root Primitive) (func(), error) {
|
|
357
|
+
frameworkVersion, versionErr := termwrightFrameworkVersion()
|
|
358
|
+
if versionErr != nil {
|
|
359
|
+
return nil, versionErr
|
|
360
|
+
}
|
|
361
|
+
p := newTermwrightProbe(frameworkVersion)
|
|
362
|
+
if p == nil {
|
|
363
|
+
return func() {}, nil
|
|
364
|
+
}
|
|
365
|
+
if err := p.connect(); err != nil {
|
|
366
|
+
_ = p.client.Close()
|
|
367
|
+
return nil, err
|
|
368
|
+
}
|
|
369
|
+
p.application = a
|
|
370
|
+
|
|
371
|
+
decorated, installErr := termwrightInstallScreen(a, p)
|
|
372
|
+
if installErr != nil {
|
|
373
|
+
p.close()
|
|
374
|
+
return nil, installErr
|
|
375
|
+
}
|
|
376
|
+
termwrightLastProbe.Store(p)
|
|
377
|
+
return func() {
|
|
378
|
+
decorated.detach()
|
|
379
|
+
p.close()
|
|
380
|
+
}, nil
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// termwrightInstallScreen installs the decorator directly while Attach still
|
|
384
|
+
// owns the pre-Run lifecycle. Application.SetScreen cannot replace an already
|
|
385
|
+
// configured pre-Run screen: it finalizes it and queues an asynchronous runtime
|
|
386
|
+
// replacement. T1 access avoids that behavior without touching upstream code.
|
|
387
|
+
func termwrightInstallScreen(a *Application, p *termwrightProbeState) (*termwrightScreen, error) {
|
|
388
|
+
a.Lock()
|
|
389
|
+
defer a.Unlock()
|
|
390
|
+
if existing, ok := a.screen.(*termwrightScreen); ok && !existing.detached.Load() {
|
|
391
|
+
return nil, errors.New("termwright tview probe is already attached to this Application")
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
underlying := a.screen
|
|
395
|
+
owned := false
|
|
396
|
+
if underlying == nil {
|
|
397
|
+
var err error
|
|
398
|
+
underlying, err = tcell.NewScreen()
|
|
399
|
+
if err != nil {
|
|
400
|
+
return nil, err
|
|
401
|
+
}
|
|
402
|
+
if err = underlying.Init(); err != nil {
|
|
403
|
+
return nil, err
|
|
404
|
+
}
|
|
405
|
+
owned = true
|
|
406
|
+
if a.enableMouse {
|
|
407
|
+
underlying.EnableMouse()
|
|
408
|
+
} else {
|
|
409
|
+
underlying.DisableMouse()
|
|
410
|
+
}
|
|
411
|
+
if a.enablePaste {
|
|
412
|
+
underlying.EnablePaste()
|
|
413
|
+
} else {
|
|
414
|
+
underlying.DisablePaste()
|
|
415
|
+
}
|
|
416
|
+
if a.title != "" {
|
|
417
|
+
underlying.SetTitle(a.title)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
decorated := &termwrightScreen{
|
|
421
|
+
Screen: underlying,
|
|
422
|
+
application: a,
|
|
423
|
+
owned: owned,
|
|
424
|
+
commit: p.afterFrame,
|
|
425
|
+
fail: func(code, message string) {
|
|
426
|
+
if publisher := p.publisher.Load(); publisher != nil {
|
|
427
|
+
publisher.Fail(code, message)
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
}
|
|
431
|
+
decorated.previousBefore = a.beforeDraw
|
|
432
|
+
decorated.previousAfter = a.afterDraw
|
|
433
|
+
decorated.beforeHook = decorated.beforeDraw
|
|
434
|
+
decorated.afterHook = decorated.afterDraw
|
|
435
|
+
a.beforeDraw = decorated.beforeHook
|
|
436
|
+
a.afterDraw = decorated.afterHook
|
|
437
|
+
a.screen = decorated
|
|
438
|
+
return decorated, nil
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// detach restores only the screen installation this decorator still owns. A
|
|
442
|
+
// later application SetScreen wins and is never overwritten during cleanup.
|
|
443
|
+
func (s *termwrightScreen) detach() {
|
|
444
|
+
if s.cleaned.CompareAndSwap(false, true) {
|
|
445
|
+
s.application.Lock()
|
|
446
|
+
if !s.hooksIntact() {
|
|
447
|
+
s.failClosed("tview lifecycle hooks were displaced before probe cleanup")
|
|
448
|
+
}
|
|
449
|
+
s.detached.Store(true)
|
|
450
|
+
owned := s.application.screen == s
|
|
451
|
+
if termwrightSameHook(s.application.beforeDraw, s.beforeHook) {
|
|
452
|
+
s.application.beforeDraw = s.previousBefore
|
|
453
|
+
}
|
|
454
|
+
if termwrightSameHook(s.application.afterDraw, s.afterHook) {
|
|
455
|
+
s.application.afterDraw = s.previousAfter
|
|
456
|
+
}
|
|
457
|
+
if owned {
|
|
458
|
+
if s.owned {
|
|
459
|
+
s.application.screen = nil
|
|
460
|
+
} else {
|
|
461
|
+
s.application.screen = s.Screen
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
s.application.Unlock()
|
|
465
|
+
if owned && s.owned {
|
|
466
|
+
s.finishUnderlying()
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// The publication queue is closed immediately after detach. Do not let a
|
|
470
|
+
// deferred lifecycle diagnostic race that close or outlive probe cleanup.
|
|
471
|
+
s.diagnosis.Wait()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
func termwrightFrameworkVersion() (string, error) {
|
|
475
|
+
build, ok := debug.ReadBuildInfo()
|
|
476
|
+
if !ok {
|
|
477
|
+
return "", errors.New("termwright tview probe cannot read the application build graph")
|
|
478
|
+
}
|
|
479
|
+
for _, dependency := range build.Deps {
|
|
480
|
+
if dependency.Path != "github.com/rivo/tview" {
|
|
481
|
+
continue
|
|
482
|
+
}
|
|
483
|
+
return termwrightDependencyVersion(dependency), nil
|
|
484
|
+
}
|
|
485
|
+
return "", errors.New("termwright tview probe did not find tview in the application build graph")
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
func termwrightDependencyVersion(dependency *debug.Module) string {
|
|
489
|
+
version := dependency.Version
|
|
490
|
+
if dependency.Replace != nil && dependency.Replace.Version != "" {
|
|
491
|
+
version = dependency.Replace.Version
|
|
492
|
+
}
|
|
493
|
+
if version == "" || version == "(devel)" {
|
|
494
|
+
// Version is advisory for a T1 integration. The compiler has already
|
|
495
|
+
// checked the actual private-symbol contract, so a workspace/local module
|
|
496
|
+
// must not be rejected merely because build metadata calls it devel.
|
|
497
|
+
return "capability-local"
|
|
498
|
+
}
|
|
499
|
+
return version
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
func (p *termwrightProbeState) close() {
|
|
503
|
+
p.shutdown.Do(func() {
|
|
504
|
+
p.recoveryStopOnce.Do(func() { close(p.recoveryStop) })
|
|
505
|
+
if publisher := p.publisher.Swap(nil); publisher != nil {
|
|
506
|
+
publisher.Shutdown()
|
|
507
|
+
} else {
|
|
508
|
+
_ = p.client.Close()
|
|
509
|
+
}
|
|
510
|
+
p.recoveryWorkers.Wait()
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// afterFrame is the method form, so a test can drive an instance of its own
|
|
515
|
+
// rather than the package-level probe a real run installs.
|
|
516
|
+
func (p *termwrightProbeState) afterFrame(root Primitive, screen tcell.Screen) {
|
|
517
|
+
if screen == nil {
|
|
518
|
+
return
|
|
519
|
+
}
|
|
520
|
+
if !p.client.Connected() {
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
columns, rows := screen.Size()
|
|
524
|
+
if columns <= 0 || rows <= 0 {
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
snapshot, duplicateKey, snapshotErr := p.snapshot(root, columns, rows)
|
|
529
|
+
if snapshotErr != nil {
|
|
530
|
+
p.onPublishFailed(p.publisher.Load(), snapshotErr)
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
if duplicateKey != "" {
|
|
534
|
+
if publisher := p.publisher.Load(); publisher != nil {
|
|
535
|
+
publisher.Fail("duplicate-semantic-key", "duplicate SemanticKey: "+string(duplicateKey))
|
|
536
|
+
}
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
publisher := p.publisher.Load()
|
|
540
|
+
if publisher == nil {
|
|
541
|
+
return
|
|
542
|
+
}
|
|
543
|
+
p.publishFrame(publisher, snapshot, screen)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// publishFrame is the deterministic adapter boundary between a completed
|
|
547
|
+
// framework snapshot and the same-writer marker. Keeping it separate lets the
|
|
548
|
+
// adapter test inject a controlled refusal instead of relying on host socket
|
|
549
|
+
// buffer saturation.
|
|
550
|
+
func (p *termwrightProbeState) publishFrame(publisher termwrightPublicationSink, snapshot *protocol.Snapshot, screen tcell.Screen) {
|
|
551
|
+
marker, err := publisher.TryPublish(snapshot)
|
|
552
|
+
if err != nil || marker == "" {
|
|
553
|
+
p.onPublishFailed(publisher, err)
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
// Show and the marker use the same terminal writer. Publication is only a
|
|
557
|
+
// committed frame once all marker bytes have followed the screen bytes.
|
|
558
|
+
if writeErr := termwrightWriteMarker(screen, marker); writeErr != nil {
|
|
559
|
+
p.dropped.Add(1)
|
|
560
|
+
publisher.Fail("adapter-guarantee-violation", "tview could not write the complete render marker through the screen commit writer")
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
p.frames.Add(1)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
type termwrightWindowsMarkerScreen interface {
|
|
567
|
+
TermwrightWriteMarker(string) error
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
func termwrightWriteMarker(screen tcell.Screen, marker string) error {
|
|
571
|
+
if terminal, ok := screen.Tty(); ok && terminal != nil {
|
|
572
|
+
written, err := terminal.Write([]byte(marker))
|
|
573
|
+
if err != nil {
|
|
574
|
+
return err
|
|
575
|
+
}
|
|
576
|
+
if written != len(marker) {
|
|
577
|
+
return errors.New("tcell terminal accepted a partial render marker")
|
|
578
|
+
}
|
|
579
|
+
return nil
|
|
580
|
+
}
|
|
581
|
+
if windows, ok := screen.(termwrightWindowsMarkerScreen); ok {
|
|
582
|
+
return windows.TermwrightWriteMarker(marker)
|
|
583
|
+
}
|
|
584
|
+
return errors.New("tcell screen exposes no writer used by Show")
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// onPublishFailed records a frame the driver will never see.
|
|
588
|
+
//
|
|
589
|
+
// Two things have to happen together, and leaving out either one
|
|
590
|
+
// produces a worse failure than dropping the frame did:
|
|
591
|
+
//
|
|
592
|
+
// 1. **No marker.** A marker names a revision; writing one for a tree that
|
|
593
|
+
// never arrived makes the driver wait for it and then report
|
|
594
|
+
// revision-pairing-watchdog — a diagnosis pointing at the adapter's timing rather
|
|
595
|
+
// than at the driver that stopped reading.
|
|
596
|
+
// 2. **Keep rendering.** The application is mid-frame under its own lock;
|
|
597
|
+
// instrumentation failing is not the application failing.
|
|
598
|
+
func (p *termwrightProbeState) onPublishFailed(publisher termwrightPublicationSink, err error) {
|
|
599
|
+
p.dropped.Add(1)
|
|
600
|
+
if errors.Is(err, protocol.ErrPublicationQueueFull) && publisher != nil {
|
|
601
|
+
p.requestAuthoritativeRedraw(publisher.ReadyAfterDrop())
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
if errors.Is(err, protocol.ErrPublicationQueueBusy) {
|
|
605
|
+
// Busy is a non-blocking admission refusal: a lifecycle or worker edge
|
|
606
|
+
// briefly owned one of the queue's locks. Defer a fresh draw to tview's
|
|
607
|
+
// event loop instead of turning that expected contention into a fatal or
|
|
608
|
+
// waiting in Screen.Show. The current snapshot receives no revision or
|
|
609
|
+
// marker and is never replayed.
|
|
610
|
+
ready := make(chan struct{})
|
|
611
|
+
close(ready)
|
|
612
|
+
p.requestAuthoritativeRedraw(ready)
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
message := "tview rendered a frame that semantic publication did not admit"
|
|
616
|
+
if err != nil {
|
|
617
|
+
message += ": " + err.Error()
|
|
618
|
+
}
|
|
619
|
+
if publisher != nil {
|
|
620
|
+
publisher.Fail("adapter-guarantee-violation", message)
|
|
621
|
+
}
|
|
622
|
+
if errors.Is(err, protocol.ErrPublicationWorkerFailed) {
|
|
623
|
+
// The worker closes an unrecoverable stream and admits no later marker.
|
|
624
|
+
// This counter retains its public name for probe statistics, but no
|
|
625
|
+
// transport deadline runs on the render thread anymore.
|
|
626
|
+
p.timedOut.Add(1)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// requestAuthoritativeRedraw turns bounded-queue loss into an explicit,
|
|
631
|
+
// causal recovery. The render callback only records the drop and starts one
|
|
632
|
+
// waiter. Once the publication worker has dequeued an item, that waiter queues
|
|
633
|
+
// a fresh framework draw on tview's own event loop. The fresh draw re-reads the
|
|
634
|
+
// retained widget tree and emits its own same-writer marker; the rejected
|
|
635
|
+
// revision is never replayed or assigned a number.
|
|
636
|
+
func (p *termwrightProbeState) requestAuthoritativeRedraw(ready <-chan struct{}) {
|
|
637
|
+
if p.application == nil || ready == nil || p.recoveryStop == nil {
|
|
638
|
+
return
|
|
639
|
+
}
|
|
640
|
+
if !p.recoveryPending.CompareAndSwap(false, true) {
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
p.recoveryWorkers.Add(1)
|
|
644
|
+
go func() {
|
|
645
|
+
defer p.recoveryWorkers.Done()
|
|
646
|
+
select {
|
|
647
|
+
case <-ready:
|
|
648
|
+
case <-p.recoveryStop:
|
|
649
|
+
p.recoveryPending.Store(false)
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
redraw := queuedUpdate{f: func() {
|
|
654
|
+
p.recoveryPending.Store(false)
|
|
655
|
+
select {
|
|
656
|
+
case <-p.recoveryStop:
|
|
657
|
+
return
|
|
658
|
+
default:
|
|
659
|
+
p.application.draw()
|
|
660
|
+
}
|
|
661
|
+
}}
|
|
662
|
+
select {
|
|
663
|
+
case p.application.updates <- redraw:
|
|
664
|
+
case <-p.recoveryStop:
|
|
665
|
+
p.recoveryPending.Store(false)
|
|
666
|
+
}
|
|
667
|
+
}()
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// identity returns a stable id for a primitive.
|
|
671
|
+
//
|
|
672
|
+
// The pointer is the identity: tview retains its widget tree across frames, so
|
|
673
|
+
// the same *Button is the same button, and that is what makes the IR's
|
|
674
|
+
// `stable` identity kind honest here rather than a fabricated ordinal.
|
|
675
|
+
func (p *termwrightProbeState) identity(primitive Primitive) (string, error) {
|
|
676
|
+
typeOf := reflect.TypeOf(primitive)
|
|
677
|
+
if typeOf == nil || !typeOf.Comparable() {
|
|
678
|
+
return "", errors.New("tview custom Primitive has no stable comparable session identity: " + termwrightTypeName(primitive))
|
|
679
|
+
}
|
|
680
|
+
p.mu.Lock()
|
|
681
|
+
defer p.mu.Unlock()
|
|
682
|
+
if id, ok := p.ids[primitive]; ok {
|
|
683
|
+
return id, nil
|
|
684
|
+
}
|
|
685
|
+
p.nextID++
|
|
686
|
+
id := "n" + strconv.Itoa(p.nextID)
|
|
687
|
+
p.ids[primitive] = id
|
|
688
|
+
return id, nil
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// snapshot walks the retained tree into the wire form.
|
|
692
|
+
func (p *termwrightProbeState) snapshot(root Primitive, columns, rows int) (*protocol.Snapshot, annotate.SemanticKey, error) {
|
|
693
|
+
snapshot := protocol.NewSnapshot("", 0, columns, rows)
|
|
694
|
+
snapshot.HitGrid = termwrightUnsupportedHitGrid()
|
|
695
|
+
if root == nil {
|
|
696
|
+
return snapshot, "", nil
|
|
697
|
+
}
|
|
698
|
+
keys := make(map[annotate.SemanticKey]string)
|
|
699
|
+
duplicates := make(map[annotate.SemanticKey]struct{})
|
|
700
|
+
pending := make([]termwrightRelations, 0)
|
|
701
|
+
if err := p.walk(root, "", false, columns, rows, snapshot, keys, duplicates, &pending); err != nil {
|
|
702
|
+
return snapshot, "", err
|
|
703
|
+
}
|
|
704
|
+
if len(duplicates) > 0 {
|
|
705
|
+
ordered := make([]string, 0, len(duplicates))
|
|
706
|
+
for key := range duplicates {
|
|
707
|
+
ordered = append(ordered, string(key))
|
|
708
|
+
}
|
|
709
|
+
sort.Strings(ordered)
|
|
710
|
+
return snapshot, annotate.SemanticKey(ordered[0]), nil
|
|
711
|
+
}
|
|
712
|
+
maxRelations := protocol.DefaultLimits.MaxRelationTargets
|
|
713
|
+
if p.client != nil {
|
|
714
|
+
maxRelations = p.client.Limits().MaxRelationTargets
|
|
715
|
+
}
|
|
716
|
+
termwrightResolveRelations(snapshot, keys, duplicates, pending, maxRelations)
|
|
717
|
+
return snapshot, "", nil
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// termwrightRelations holds author references until every node has been
|
|
721
|
+
// visited. A label may be drawn after the control it labels, so resolving in
|
|
722
|
+
// walk order would make declaration order part of the API.
|
|
723
|
+
type termwrightRelations struct {
|
|
724
|
+
nodeIndex int
|
|
725
|
+
labelledBy []annotate.SemanticKey
|
|
726
|
+
describedBy []annotate.SemanticKey
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// walk appends one node and recurses.
|
|
730
|
+
//
|
|
731
|
+
// `hidden` is inherited: a widget on an unshown page is not merely unfocused,
|
|
732
|
+
// it is not on screen, and every descendant of it is in the same position.
|
|
733
|
+
func (p *termwrightProbeState) walk(
|
|
734
|
+
primitive Primitive,
|
|
735
|
+
parentID string,
|
|
736
|
+
hidden bool,
|
|
737
|
+
columns, rows int,
|
|
738
|
+
snapshot *protocol.Snapshot,
|
|
739
|
+
keys map[annotate.SemanticKey]string,
|
|
740
|
+
duplicates map[annotate.SemanticKey]struct{},
|
|
741
|
+
pending *[]termwrightRelations,
|
|
742
|
+
) error {
|
|
743
|
+
if primitive == nil {
|
|
744
|
+
return nil
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
id, identityErr := p.identity(primitive)
|
|
748
|
+
if identityErr != nil {
|
|
749
|
+
return identityErr
|
|
750
|
+
}
|
|
751
|
+
children := termwrightChildren(primitive)
|
|
752
|
+
|
|
753
|
+
// HasFocus reports true for ancestors of the focused primitive as well, so
|
|
754
|
+
// the flag belongs to the deepest one that claims it.
|
|
755
|
+
focused := !hidden && primitive.HasFocus() && !termwrightAnyFocus(children)
|
|
756
|
+
|
|
757
|
+
role := termwrightRole(primitive)
|
|
758
|
+
node := protocol.Node{
|
|
759
|
+
ID: id,
|
|
760
|
+
ParentID: parentID,
|
|
761
|
+
Role: role,
|
|
762
|
+
Name: termwrightName(primitive),
|
|
763
|
+
Value: termwrightValue(primitive),
|
|
764
|
+
State: termwrightState(primitive, focused, hidden),
|
|
765
|
+
P: protocol.ProvenanceFramework,
|
|
766
|
+
PX: map[string]string{
|
|
767
|
+
"role": protocol.ProvenanceRecognizer,
|
|
768
|
+
},
|
|
769
|
+
}
|
|
770
|
+
node.Actions, node.InputRecipes = termwrightPhysicalSemantics(primitive)
|
|
771
|
+
if len(node.Actions) > 0 {
|
|
772
|
+
termwrightProvenance(&node, "actions", protocol.ProvenanceFramework)
|
|
773
|
+
}
|
|
774
|
+
if len(node.InputRecipes) > 0 {
|
|
775
|
+
termwrightProvenance(&node, "inputRecipes", protocol.ProvenanceFramework)
|
|
776
|
+
}
|
|
777
|
+
node.Geometry = termwrightGeometry(primitive, hidden)
|
|
778
|
+
// Required for a generic node, and useful on every other one: it is what
|
|
779
|
+
// keeps a widget this probe does not know about alive and identifiable
|
|
780
|
+
// rather than flattened into an anonymous region.
|
|
781
|
+
node.FrameworkType = termwrightTypeName(primitive)
|
|
782
|
+
node.OpaqueChildren = !termwrightKnownPrimitive(primitive)
|
|
783
|
+
// An author's annotation is merged on top of the observed facts, and only
|
|
784
|
+
// where the probe has nothing better: it may say what a widget *is*, never
|
|
785
|
+
// where it is or whether it has the focus. Those the probe measured.
|
|
786
|
+
meta, annotated := annotate.Lookup(primitive)
|
|
787
|
+
if annotated {
|
|
788
|
+
termwrightApplyAnnotation(meta, &node)
|
|
789
|
+
termwrightSortActions(node.Actions)
|
|
790
|
+
termwrightRegisterKey(meta.Key, id, keys, duplicates)
|
|
791
|
+
}
|
|
792
|
+
// Unknown primitives remain explicit generic nodes with their concrete
|
|
793
|
+
// framework type and typed opaqueChildren marker. Extended remains
|
|
794
|
+
// exclusively application-defined state.
|
|
795
|
+
if parentID == "" {
|
|
796
|
+
snapshot.RootIDs = append(snapshot.RootIDs, id)
|
|
797
|
+
}
|
|
798
|
+
snapshot.Nodes = append(snapshot.Nodes, node)
|
|
799
|
+
if annotated && (len(meta.LabelledBy) > 0 || len(meta.DescribedBy) > 0) {
|
|
800
|
+
*pending = append(*pending, termwrightRelations{
|
|
801
|
+
nodeIndex: len(snapshot.Nodes) - 1,
|
|
802
|
+
labelledBy: meta.LabelledBy,
|
|
803
|
+
describedBy: meta.DescribedBy,
|
|
804
|
+
})
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
for _, child := range children {
|
|
808
|
+
if err := p.walk(child.primitive, id, hidden || child.hidden, columns, rows, snapshot, keys, duplicates, pending); err != nil {
|
|
809
|
+
return err
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
p.appendSynthetic(primitive, id, hidden, snapshot)
|
|
813
|
+
return nil
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// termwrightKnownPrimitive distinguishes built-in leaves from author-defined
|
|
817
|
+
// primitives whose possible children tview does not expose. Unknown values are
|
|
818
|
+
// retained as explicit opaque nodes instead of silently presenting them as
|
|
819
|
+
// complete leaves.
|
|
820
|
+
func termwrightKnownPrimitive(primitive Primitive) bool {
|
|
821
|
+
switch primitive.(type) {
|
|
822
|
+
case *Box, *Button, *Checkbox, *DropDown, *Flex, *Form, *Frame, *Grid,
|
|
823
|
+
*Image, *InputField, *List, *Modal, *Pages, *Table,
|
|
824
|
+
*TextArea, *TextView, *TreeView:
|
|
825
|
+
return true
|
|
826
|
+
default:
|
|
827
|
+
return false
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
func termwrightRegisterKey(
|
|
832
|
+
key annotate.SemanticKey,
|
|
833
|
+
id string,
|
|
834
|
+
keys map[annotate.SemanticKey]string,
|
|
835
|
+
duplicates map[annotate.SemanticKey]struct{},
|
|
836
|
+
) {
|
|
837
|
+
if key == "" {
|
|
838
|
+
return
|
|
839
|
+
}
|
|
840
|
+
if _, duplicate := duplicates[key]; duplicate {
|
|
841
|
+
return
|
|
842
|
+
}
|
|
843
|
+
if previous, exists := keys[key]; exists && previous != id {
|
|
844
|
+
delete(keys, key)
|
|
845
|
+
duplicates[key] = struct{}{}
|
|
846
|
+
return
|
|
847
|
+
}
|
|
848
|
+
keys[key] = id
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
func termwrightResolveRelations(
|
|
852
|
+
snapshot *protocol.Snapshot,
|
|
853
|
+
keys map[annotate.SemanticKey]string,
|
|
854
|
+
duplicates map[annotate.SemanticKey]struct{},
|
|
855
|
+
pending []termwrightRelations,
|
|
856
|
+
maxRelations int,
|
|
857
|
+
) {
|
|
858
|
+
resolve := func(references []annotate.SemanticKey) []string {
|
|
859
|
+
resolved := make([]string, 0, len(references))
|
|
860
|
+
seen := make(map[string]struct{}, len(references))
|
|
861
|
+
for _, key := range references {
|
|
862
|
+
if len(resolved) >= maxRelations {
|
|
863
|
+
break
|
|
864
|
+
}
|
|
865
|
+
if _, duplicate := duplicates[key]; duplicate {
|
|
866
|
+
continue
|
|
867
|
+
}
|
|
868
|
+
id, found := keys[key]
|
|
869
|
+
if !found {
|
|
870
|
+
continue
|
|
871
|
+
}
|
|
872
|
+
if _, repeated := seen[id]; repeated {
|
|
873
|
+
continue
|
|
874
|
+
}
|
|
875
|
+
seen[id] = struct{}{}
|
|
876
|
+
resolved = append(resolved, id)
|
|
877
|
+
}
|
|
878
|
+
return resolved
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
for _, relation := range pending {
|
|
882
|
+
node := &snapshot.Nodes[relation.nodeIndex]
|
|
883
|
+
if ids := resolve(relation.labelledBy); len(ids) > 0 {
|
|
884
|
+
node.LabelledBy = ids
|
|
885
|
+
termwrightProvenance(node, "labelledBy", protocol.ProvenanceAnnotation)
|
|
886
|
+
}
|
|
887
|
+
if ids := resolve(relation.describedBy); len(ids) > 0 {
|
|
888
|
+
node.DescribedBy = ids
|
|
889
|
+
termwrightProvenance(node, "describedBy", protocol.ProvenanceAnnotation)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
func termwrightProvenance(node *protocol.Node, field, source string) {
|
|
895
|
+
if node.PX == nil {
|
|
896
|
+
node.PX = make(map[string]string)
|
|
897
|
+
}
|
|
898
|
+
node.PX[field] = source
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// termwrightApplyAnnotation merges what the application declared.
|
|
902
|
+
//
|
|
903
|
+
// tview retains its widgets, so a registry keyed by the primitive's identity
|
|
904
|
+
// works here — which is why tview annotates by registration while Charm, whose
|
|
905
|
+
// components are copied values, annotates through an interface.
|
|
906
|
+
func termwrightApplyAnnotation(meta annotate.Semantics, node *protocol.Node) {
|
|
907
|
+
if meta.Role != "" {
|
|
908
|
+
// Validated against the closed set and dropped when unknown, rather
|
|
909
|
+
// than guessed at: exhaustive switches downstream depend on that set
|
|
910
|
+
// staying closed, and a typo in an annotation is the author's to fix.
|
|
911
|
+
if role := protocol.Role(meta.Role); protocol.ValidRole(role) {
|
|
912
|
+
node.Role = role
|
|
913
|
+
termwrightProvenance(node, "role", protocol.ProvenanceAnnotation)
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if meta.Name != "" {
|
|
917
|
+
node.Name = meta.Name
|
|
918
|
+
termwrightProvenance(node, "name", protocol.ProvenanceAnnotation)
|
|
919
|
+
}
|
|
920
|
+
if meta.TestID != "" {
|
|
921
|
+
node.TestID = meta.TestID
|
|
922
|
+
termwrightProvenance(node, "testId", protocol.ProvenanceAnnotation)
|
|
923
|
+
}
|
|
924
|
+
if meta.Description != "" {
|
|
925
|
+
node.Description = meta.Description
|
|
926
|
+
termwrightProvenance(node, "description", protocol.ProvenanceAnnotation)
|
|
927
|
+
}
|
|
928
|
+
// Domain state has its own namespace, so it cannot pollute the closed
|
|
929
|
+
// portable state vocabulary or masquerade as prose.
|
|
930
|
+
if len(meta.Domain) > 0 {
|
|
931
|
+
node.Extended = make(map[string]any, len(meta.Domain))
|
|
932
|
+
for key, value := range meta.Domain {
|
|
933
|
+
node.Extended[key] = value
|
|
934
|
+
}
|
|
935
|
+
termwrightProvenance(node, "extended", protocol.ProvenanceAnnotation)
|
|
936
|
+
}
|
|
937
|
+
seenActions := make(map[protocol.Action]struct{}, len(node.Actions)+len(meta.Actions))
|
|
938
|
+
for _, action := range node.Actions {
|
|
939
|
+
seenActions[action] = struct{}{}
|
|
940
|
+
}
|
|
941
|
+
annotationDeclaredAction := false
|
|
942
|
+
for _, action := range meta.Actions {
|
|
943
|
+
if !protocol.ValidAction(action) {
|
|
944
|
+
continue
|
|
945
|
+
}
|
|
946
|
+
annotationDeclaredAction = true
|
|
947
|
+
if _, duplicate := seenActions[action]; duplicate {
|
|
948
|
+
continue
|
|
949
|
+
}
|
|
950
|
+
seenActions[action] = struct{}{}
|
|
951
|
+
node.Actions = append(node.Actions, action)
|
|
952
|
+
}
|
|
953
|
+
if annotationDeclaredAction {
|
|
954
|
+
termwrightProvenance(node, "actions", protocol.ProvenanceAnnotation)
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// termwrightPhysicalSemantics publishes only keybindings proven by the current
|
|
959
|
+
// conformance profile's InputHandler implementations. Candidate certification
|
|
960
|
+
// re-verifies these behaviors. They are data recipes executed later
|
|
961
|
+
// through the real PTY; this instrumentation never invokes a handler.
|
|
962
|
+
func termwrightPhysicalSemantics(p Primitive) ([]protocol.Action, []protocol.PhysicalInputRecipe) {
|
|
963
|
+
press := func(action protocol.Action, key string) ([]protocol.Action, []protocol.PhysicalInputRecipe) {
|
|
964
|
+
return []protocol.Action{action}, []protocol.PhysicalInputRecipe{{
|
|
965
|
+
Action: string(action), RequiresFocus: true,
|
|
966
|
+
Steps: []protocol.PhysicalInputRecipeStep{{Kind: "press", Key: key}},
|
|
967
|
+
}}
|
|
968
|
+
}
|
|
969
|
+
switch p.(type) {
|
|
970
|
+
case *Button:
|
|
971
|
+
return press(protocol.ActionActivate, "Enter")
|
|
972
|
+
case *Checkbox:
|
|
973
|
+
return press(protocol.ActionToggle, "Space")
|
|
974
|
+
default:
|
|
975
|
+
return nil, nil
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
func termwrightSortActions(actions []protocol.Action) {
|
|
980
|
+
order := map[protocol.Action]int{
|
|
981
|
+
protocol.ActionFocus: 0, protocol.ActionActivate: 1, protocol.ActionToggle: 2,
|
|
982
|
+
protocol.ActionSetValue: 3, protocol.ActionScroll: 4, protocol.ActionSelect: 5,
|
|
983
|
+
protocol.ActionExpand: 6,
|
|
984
|
+
}
|
|
985
|
+
sort.SliceStable(actions, func(left, right int) bool { return order[actions[left]] < order[actions[right]] })
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// termwrightChild is a child plus whether its container is showing it.
|
|
989
|
+
type termwrightChild struct {
|
|
990
|
+
primitive Primitive
|
|
991
|
+
hidden bool
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// termwrightChildren enumerates a container's children from inside the package.
|
|
995
|
+
func termwrightChildren(p Primitive) []termwrightChild {
|
|
996
|
+
switch c := p.(type) {
|
|
997
|
+
case *Flex:
|
|
998
|
+
children := make([]termwrightChild, 0, c.GetItemCount())
|
|
999
|
+
for index := 0; index < c.GetItemCount(); index++ {
|
|
1000
|
+
if item := c.GetItem(index); item != nil {
|
|
1001
|
+
children = append(children, termwrightChild{primitive: item})
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return children
|
|
1005
|
+
case *Grid:
|
|
1006
|
+
// The case an out-of-package adapter cannot serve at all. `visible`
|
|
1007
|
+
// carries the last draw's decision, which is exactly what a test means
|
|
1008
|
+
// by "is it on screen".
|
|
1009
|
+
children := make([]termwrightChild, 0, len(c.items))
|
|
1010
|
+
for _, item := range c.items {
|
|
1011
|
+
if item.Item != nil {
|
|
1012
|
+
children = append(children, termwrightChild{primitive: item.Item, hidden: !item.visible})
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return children
|
|
1016
|
+
case *Pages:
|
|
1017
|
+
visible := make(map[string]struct{}, c.GetPageCount())
|
|
1018
|
+
for _, name := range c.GetPageNames(true) {
|
|
1019
|
+
visible[name] = struct{}{}
|
|
1020
|
+
}
|
|
1021
|
+
// GetPageNames returns front-to-back; reverse it to preserve Draw's
|
|
1022
|
+
// public back-to-front paint order without reading Pages.pages.
|
|
1023
|
+
names := c.GetPageNames(false)
|
|
1024
|
+
children := make([]termwrightChild, 0, len(names))
|
|
1025
|
+
for index := len(names) - 1; index >= 0; index-- {
|
|
1026
|
+
name := names[index]
|
|
1027
|
+
if item := c.GetPage(name); item != nil {
|
|
1028
|
+
_, shown := visible[name]
|
|
1029
|
+
children = append(children, termwrightChild{primitive: item, hidden: !shown})
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return children
|
|
1033
|
+
case *Frame:
|
|
1034
|
+
if primitive := c.GetPrimitive(); primitive != nil {
|
|
1035
|
+
return []termwrightChild{{primitive: primitive}}
|
|
1036
|
+
}
|
|
1037
|
+
case *Form:
|
|
1038
|
+
children := make([]termwrightChild, 0, c.GetFormItemCount()+c.GetButtonCount())
|
|
1039
|
+
for index := 0; index < c.GetFormItemCount(); index++ {
|
|
1040
|
+
children = append(children, termwrightChild{primitive: c.GetFormItem(index)})
|
|
1041
|
+
}
|
|
1042
|
+
for index := 0; index < c.GetButtonCount(); index++ {
|
|
1043
|
+
children = append(children, termwrightChild{primitive: c.GetButton(index)})
|
|
1044
|
+
}
|
|
1045
|
+
return children
|
|
1046
|
+
case *Modal:
|
|
1047
|
+
if c.frame != nil {
|
|
1048
|
+
return []termwrightChild{{primitive: c.frame}}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return nil
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
func termwrightAnyFocus(children []termwrightChild) bool {
|
|
1055
|
+
for _, child := range children {
|
|
1056
|
+
if child.primitive != nil && child.primitive.HasFocus() {
|
|
1057
|
+
return true
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return false
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// termwrightRole maps a widget type to the closed role set.
|
|
1064
|
+
//
|
|
1065
|
+
// Deliberately identical to the hand-written adapter's mapping: the two must
|
|
1066
|
+
// agree, or the same application would describe itself differently depending
|
|
1067
|
+
// on how it was instrumented, and every conformance snapshot would fork.
|
|
1068
|
+
func termwrightRole(p Primitive) protocol.Role {
|
|
1069
|
+
switch p.(type) {
|
|
1070
|
+
case *Button:
|
|
1071
|
+
return protocol.RoleButton
|
|
1072
|
+
case *Checkbox:
|
|
1073
|
+
return protocol.RoleCheckbox
|
|
1074
|
+
case *InputField, *TextArea:
|
|
1075
|
+
return protocol.RoleTextbox
|
|
1076
|
+
case *DropDown, *List, *TreeView:
|
|
1077
|
+
return protocol.RoleList
|
|
1078
|
+
case *Table:
|
|
1079
|
+
return protocol.RoleTable
|
|
1080
|
+
case *TextView:
|
|
1081
|
+
return protocol.RoleText
|
|
1082
|
+
case *Modal:
|
|
1083
|
+
return protocol.RoleDialog
|
|
1084
|
+
case *Form, *Flex, *Grid, *Pages, *Frame, *Box:
|
|
1085
|
+
return protocol.RoleRegion
|
|
1086
|
+
}
|
|
1087
|
+
// Never dropped: an unrecognised widget keeps its geometry, its children and
|
|
1088
|
+
// its own type name, which is what makes a new tview release degrade
|
|
1089
|
+
// rather than disappear.
|
|
1090
|
+
return protocol.RoleGeneric
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// termwrightName derives the accessible name.
|
|
1094
|
+
func termwrightName(p Primitive) string {
|
|
1095
|
+
switch widget := p.(type) {
|
|
1096
|
+
case *Button:
|
|
1097
|
+
return widget.GetLabel()
|
|
1098
|
+
case *Checkbox:
|
|
1099
|
+
return termwrightFirst(widget.GetLabel(), widget.GetTitle())
|
|
1100
|
+
case *InputField:
|
|
1101
|
+
return termwrightFirst(widget.GetLabel(), widget.GetTitle())
|
|
1102
|
+
case *DropDown:
|
|
1103
|
+
return termwrightFirst(widget.GetLabel(), widget.GetTitle())
|
|
1104
|
+
case *TextArea:
|
|
1105
|
+
return termwrightFirst(widget.GetLabel(), widget.GetTitle())
|
|
1106
|
+
case *TextView:
|
|
1107
|
+
return termwrightFirst(widget.GetTitle(), termwrightTrim(widget.GetText(true)))
|
|
1108
|
+
case *Modal:
|
|
1109
|
+
// Modal exposes no getter at all; the text is the only name it has.
|
|
1110
|
+
return termwrightTrim(widget.text)
|
|
1111
|
+
case *Box:
|
|
1112
|
+
return widget.GetTitle()
|
|
1113
|
+
}
|
|
1114
|
+
if boxed, ok := p.(interface{ GetTitle() string }); ok {
|
|
1115
|
+
return boxed.GetTitle()
|
|
1116
|
+
}
|
|
1117
|
+
return ""
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// termwrightValue reports the current value of a value-bearing widget.
|
|
1121
|
+
//
|
|
1122
|
+
// The observation keeps an empty public value distinct from no value. A text
|
|
1123
|
+
// transform is production evidence that the displayed widget intentionally
|
|
1124
|
+
// withholds its source text (InputField passwords use this exact mechanism),
|
|
1125
|
+
// so the probe never exports that plaintext.
|
|
1126
|
+
func termwrightValue(p Primitive) *protocol.SemanticValueObservation {
|
|
1127
|
+
switch widget := p.(type) {
|
|
1128
|
+
case *InputField:
|
|
1129
|
+
if widget.textArea != nil && widget.textArea.transform != nil {
|
|
1130
|
+
return protocol.WithheldSensitiveValue()
|
|
1131
|
+
}
|
|
1132
|
+
return protocol.PublicValue(widget.GetText(), termwrightEvidence("native"))
|
|
1133
|
+
case *TextArea:
|
|
1134
|
+
if widget.transform != nil {
|
|
1135
|
+
return protocol.WithheldSensitiveValue()
|
|
1136
|
+
}
|
|
1137
|
+
return protocol.PublicValue(widget.GetText(), termwrightEvidence("native"))
|
|
1138
|
+
case *DropDown:
|
|
1139
|
+
_, text := widget.GetCurrentOption()
|
|
1140
|
+
return protocol.PublicValue(text, termwrightEvidence("native"))
|
|
1141
|
+
}
|
|
1142
|
+
return nil
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// termwrightTypeName is the framework's own name for the widget, without the
|
|
1146
|
+
// package qualifier that would be identical on every node.
|
|
1147
|
+
func termwrightTypeName(p Primitive) string {
|
|
1148
|
+
name := reflect.TypeOf(p).String()
|
|
1149
|
+
if index := strings.LastIndex(name, "."); index >= 0 {
|
|
1150
|
+
name = name[index+1:]
|
|
1151
|
+
}
|
|
1152
|
+
return name
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// termwrightGeometry qualifies only facts the retained tview tree exposes.
|
|
1156
|
+
// GetRect is the parent's intended allocation. The framework exposes no
|
|
1157
|
+
// general nested clipping or paint ownership, so visibleRect and pointer hit
|
|
1158
|
+
// testing remain unavailable.
|
|
1159
|
+
func termwrightGeometry(p Primitive, hidden bool) protocol.NodeGeometryObservations {
|
|
1160
|
+
displayed := !hidden
|
|
1161
|
+
geometry := protocol.NodeGeometryObservations{
|
|
1162
|
+
Displayed: protocol.Observation[bool]{Status: "known", Value: &displayed, Evidence: termwrightEvidence("instrumented")},
|
|
1163
|
+
}
|
|
1164
|
+
if hidden {
|
|
1165
|
+
geometry.IntendedRect = protocol.Observation[protocol.Rect]{Status: "absent", Reason: "not-displayed", Evidence: termwrightEvidence("instrumented")}
|
|
1166
|
+
geometry.VisibleRect = protocol.Observation[protocol.Rect]{Status: "absent", Reason: "not-displayed", Evidence: termwrightEvidence("instrumented")}
|
|
1167
|
+
return geometry
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
x, y, width, height := p.GetRect()
|
|
1171
|
+
if width <= 0 || height <= 0 {
|
|
1172
|
+
geometry.IntendedRect = protocol.Observation[protocol.Rect]{Status: "absent", Reason: "not-laid-out", Evidence: termwrightEvidence("measured")}
|
|
1173
|
+
geometry.VisibleRect = protocol.Observation[protocol.Rect]{Status: "absent", Reason: "not-laid-out", Evidence: termwrightEvidence("measured")}
|
|
1174
|
+
return geometry
|
|
1175
|
+
}
|
|
1176
|
+
intended := protocol.Rect{Row: y, Column: x, Width: width, Height: height}
|
|
1177
|
+
geometry.IntendedRect = protocol.Observation[protocol.Rect]{Status: "known", Value: &intended, Evidence: termwrightEvidence("measured")}
|
|
1178
|
+
geometry.VisibleRect = protocol.Observation[protocol.Rect]{Status: "unsupported", Capability: string(protocol.CapClippedGeometry), Reason: "framework-unobservable"}
|
|
1179
|
+
return geometry
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
func termwrightEvidence(method string) *protocol.EvidenceProvenance {
|
|
1183
|
+
return &protocol.EvidenceProvenance{
|
|
1184
|
+
Source: "framework", Method: method, Strength: "authoritative", ProviderID: probeName,
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
func termwrightUnsupportedHitGrid() protocol.Observation[protocol.PointerHitGrid] {
|
|
1189
|
+
return protocol.Observation[protocol.PointerHitGrid]{
|
|
1190
|
+
Status: "unsupported", Capability: "pointer-hit-grid", Reason: "framework-unobservable",
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// termwrightCount is the same guard for set sizes.
|
|
1195
|
+
func termwrightCount(count int) *int {
|
|
1196
|
+
if count < 0 {
|
|
1197
|
+
return nil
|
|
1198
|
+
}
|
|
1199
|
+
return protocol.Int(count)
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// termwrightState reads the observable state of one widget.
|
|
1203
|
+
func termwrightState(p Primitive, focused, hidden bool) *protocol.State {
|
|
1204
|
+
state := protocol.State{}
|
|
1205
|
+
empty := true
|
|
1206
|
+
|
|
1207
|
+
if focused {
|
|
1208
|
+
state.Focused = protocol.Bool(true)
|
|
1209
|
+
empty = false
|
|
1210
|
+
}
|
|
1211
|
+
if hidden {
|
|
1212
|
+
state.Hidden = protocol.Bool(true)
|
|
1213
|
+
empty = false
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
switch widget := p.(type) {
|
|
1217
|
+
case *Button:
|
|
1218
|
+
if widget.IsDisabled() {
|
|
1219
|
+
state.Disabled = protocol.Bool(true)
|
|
1220
|
+
empty = false
|
|
1221
|
+
}
|
|
1222
|
+
case *Checkbox:
|
|
1223
|
+
state.Checked = widget.IsChecked()
|
|
1224
|
+
if widget.disabled {
|
|
1225
|
+
state.Disabled = protocol.Bool(true)
|
|
1226
|
+
}
|
|
1227
|
+
empty = false
|
|
1228
|
+
case *DropDown:
|
|
1229
|
+
if widget.disabled {
|
|
1230
|
+
state.Disabled = protocol.Bool(true)
|
|
1231
|
+
empty = false
|
|
1232
|
+
}
|
|
1233
|
+
state.SetSize = termwrightCount(widget.GetOptionCount())
|
|
1234
|
+
state.Expanded = protocol.Bool(widget.IsOpen())
|
|
1235
|
+
empty = false
|
|
1236
|
+
case *TextArea:
|
|
1237
|
+
if widget.GetDisabled() {
|
|
1238
|
+
state.Disabled = protocol.Bool(true)
|
|
1239
|
+
empty = false
|
|
1240
|
+
}
|
|
1241
|
+
case *List:
|
|
1242
|
+
state.SetSize = termwrightCount(widget.GetItemCount())
|
|
1243
|
+
empty = false
|
|
1244
|
+
case *Table:
|
|
1245
|
+
state.SetSize = termwrightCount(widget.GetRowCount())
|
|
1246
|
+
empty = false
|
|
1247
|
+
case *TreeView:
|
|
1248
|
+
state.SetSize = termwrightCount(widget.GetRowCount())
|
|
1249
|
+
empty = false
|
|
1250
|
+
case *Modal:
|
|
1251
|
+
state.Modal = protocol.Bool(true)
|
|
1252
|
+
empty = false
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
if empty {
|
|
1256
|
+
return nil
|
|
1257
|
+
}
|
|
1258
|
+
return &state
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// appendSynthetic emits nodes for entries that are not primitives of their own
|
|
1262
|
+
// — list items and dropdown options — so they are addressable by role and name.
|
|
1263
|
+
// Their geometry observations stay explicitly unknown because entries are not
|
|
1264
|
+
// primitives with framework-owned rectangles.
|
|
1265
|
+
func (p *termwrightProbeState) appendSynthetic(
|
|
1266
|
+
primitive Primitive,
|
|
1267
|
+
parentID string,
|
|
1268
|
+
hidden bool,
|
|
1269
|
+
snapshot *protocol.Snapshot,
|
|
1270
|
+
) {
|
|
1271
|
+
switch widget := primitive.(type) {
|
|
1272
|
+
case *List:
|
|
1273
|
+
current := widget.GetCurrentItem()
|
|
1274
|
+
count := widget.GetItemCount()
|
|
1275
|
+
itemOffset, _ := widget.GetOffset()
|
|
1276
|
+
x, y, width, height := widget.GetInnerRect()
|
|
1277
|
+
for index := 0; index < count; index++ {
|
|
1278
|
+
main, secondary := widget.GetItemText(index)
|
|
1279
|
+
node := protocol.Node{
|
|
1280
|
+
ID: parentID + ":item" + strconv.Itoa(index),
|
|
1281
|
+
ParentID: parentID,
|
|
1282
|
+
Role: protocol.RoleListItem,
|
|
1283
|
+
Name: termwrightFirst(main, secondary),
|
|
1284
|
+
State: termwrightItemState(index == current, index, count, hidden),
|
|
1285
|
+
P: protocol.ProvenanceFramework,
|
|
1286
|
+
PX: map[string]string{
|
|
1287
|
+
"role": protocol.ProvenanceRecognizer,
|
|
1288
|
+
},
|
|
1289
|
+
}
|
|
1290
|
+
termwrightListItemGeometry(&node, x, y, width, height, index-itemOffset, hidden)
|
|
1291
|
+
snapshot.Nodes = append(snapshot.Nodes, node)
|
|
1292
|
+
}
|
|
1293
|
+
case *DropDown:
|
|
1294
|
+
current, _ := widget.GetCurrentOption()
|
|
1295
|
+
count := widget.GetOptionCount()
|
|
1296
|
+
x, y, width, height := widget.GetRect()
|
|
1297
|
+
for index := 0; index < count; index++ {
|
|
1298
|
+
node := protocol.Node{
|
|
1299
|
+
ID: parentID + ":option" + strconv.Itoa(index),
|
|
1300
|
+
ParentID: parentID,
|
|
1301
|
+
Role: protocol.RoleListItem,
|
|
1302
|
+
Name: widget.options[index].Text,
|
|
1303
|
+
State: termwrightItemState(index == current, index, count, hidden),
|
|
1304
|
+
P: protocol.ProvenanceFramework,
|
|
1305
|
+
PX: map[string]string{
|
|
1306
|
+
"role": protocol.ProvenanceRecognizer,
|
|
1307
|
+
},
|
|
1308
|
+
}
|
|
1309
|
+
termwrightOwnedControlGeometry(&node, x, y, width, height, hidden)
|
|
1310
|
+
snapshot.Nodes = append(snapshot.Nodes, node)
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
func termwrightListItemGeometry(node *protocol.Node, x, y, width, height, row int, hidden bool) {
|
|
1316
|
+
displayed := !hidden && row >= 0 && row < height && width > 0
|
|
1317
|
+
rect := protocol.Rect{Row: y + row, Column: x, Width: width, Height: 1}
|
|
1318
|
+
node.Geometry = protocol.NodeGeometryObservations{
|
|
1319
|
+
Displayed: protocol.Observation[bool]{Status: "known", Value: &displayed, Evidence: termwrightEvidence("derived")},
|
|
1320
|
+
IntendedRect: protocol.Observation[protocol.Rect]{Status: "known", Value: &rect, Evidence: termwrightEvidence("derived")},
|
|
1321
|
+
VisibleRect: protocol.Observation[protocol.Rect]{Status: "unsupported", Capability: string(protocol.CapClippedGeometry), Reason: "framework-unobservable"},
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
func termwrightOwnedControlGeometry(node *protocol.Node, x, y, width, height int, hidden bool) {
|
|
1326
|
+
displayed := !hidden && width > 0 && height > 0
|
|
1327
|
+
rect := protocol.Rect{Row: y, Column: x, Width: width, Height: height}
|
|
1328
|
+
node.Geometry = protocol.NodeGeometryObservations{
|
|
1329
|
+
Displayed: protocol.Observation[bool]{Status: "known", Value: &displayed, Evidence: termwrightEvidence("derived")},
|
|
1330
|
+
IntendedRect: protocol.Observation[protocol.Rect]{Status: "known", Value: &rect, Evidence: termwrightEvidence("derived")},
|
|
1331
|
+
VisibleRect: protocol.Observation[protocol.Rect]{Status: "unsupported", Capability: string(protocol.CapClippedGeometry), Reason: "framework-unobservable"},
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
func termwrightItemState(selected bool, index, count int, hidden bool) *protocol.State {
|
|
1336
|
+
state := protocol.State{
|
|
1337
|
+
Selected: protocol.Bool(selected),
|
|
1338
|
+
PositionInSet: protocol.Int(index + 1),
|
|
1339
|
+
SetSize: protocol.Int(count),
|
|
1340
|
+
}
|
|
1341
|
+
if hidden {
|
|
1342
|
+
state.Hidden = protocol.Bool(true)
|
|
1343
|
+
}
|
|
1344
|
+
return &state
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
func termwrightFirst(candidates ...string) string {
|
|
1348
|
+
for _, candidate := range candidates {
|
|
1349
|
+
if trimmed := termwrightTrim(candidate); trimmed != "" {
|
|
1350
|
+
return trimmed
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return ""
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// termwrightTrim collapses the padding widgets use for layout, so a name reads
|
|
1357
|
+
// the way it looks rather than the way it was spaced.
|
|
1358
|
+
func termwrightTrim(text string) string {
|
|
1359
|
+
start := 0
|
|
1360
|
+
end := len(text)
|
|
1361
|
+
for start < end && (text[start] == ' ' || text[start] == '\t' || text[start] == '\n' || text[start] == '\r') {
|
|
1362
|
+
start++
|
|
1363
|
+
}
|
|
1364
|
+
for end > start && (text[end-1] == ' ' || text[end-1] == '\t' || text[end-1] == '\n' || text[end-1] == '\r') {
|
|
1365
|
+
end--
|
|
1366
|
+
}
|
|
1367
|
+
return text[start:end]
|
|
1368
|
+
}
|