@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
|
@@ -1,876 +0,0 @@
|
|
|
1
|
-
package tview
|
|
2
|
-
|
|
3
|
-
// Build-time instrumentation injected by termwright into a private copy of
|
|
4
|
-
// tview. Upstream never sees this file; it exists only in the copy a
|
|
5
|
-
// termwright-driven build compiles against.
|
|
6
|
-
//
|
|
7
|
-
// Being inside the package is the whole point: the semantic state a test wants
|
|
8
|
-
// — a button's label, a list's selection, a page's visibility — lives in
|
|
9
|
-
// unexported fields that no external adapter can read without reflection.
|
|
10
|
-
// Grid is the clearest case: its `items` field has no accessor at all, so the
|
|
11
|
-
// out-of-package adapter has to be handed a callback, and here it is a field
|
|
12
|
-
// read.
|
|
13
|
-
//
|
|
14
|
-
// Three rules from the Phase 0 audit (docs/architecture/audit/tview.md §1–2)
|
|
15
|
-
// are load-bearing, and breaking any of them turns a working application into
|
|
16
|
-
// a hang or a lie:
|
|
17
|
-
//
|
|
18
|
-
// 1. The hook runs inside Application.draw(), which holds the application's
|
|
19
|
-
// write lock for the whole frame. Anything that waits on the event loop —
|
|
20
|
-
// QueueUpdate, QueueUpdateDraw, Draw, SetFocus, Stop — deadlocks, because
|
|
21
|
-
// the loop is the goroutine currently inside draw().
|
|
22
|
-
// 2. Reading primitive state is safe from here and essentially nowhere else:
|
|
23
|
-
// rects are assigned by parents *during* the draw, so another goroutine
|
|
24
|
-
// reading GetRect races the layout.
|
|
25
|
-
// 3. The marker must follow the frame's bytes. The hook sits after
|
|
26
|
-
// screen.Show(), which is why publication is synchronous here rather than
|
|
27
|
-
// handed to a goroutine: a marker written later could land after the next
|
|
28
|
-
// frame's bytes and pair the tree with the wrong screen.
|
|
29
|
-
|
|
30
|
-
import (
|
|
31
|
-
"errors"
|
|
32
|
-
"os"
|
|
33
|
-
"reflect"
|
|
34
|
-
"strconv"
|
|
35
|
-
"strings"
|
|
36
|
-
"sync"
|
|
37
|
-
"sync/atomic"
|
|
38
|
-
"time"
|
|
39
|
-
|
|
40
|
-
"github.com/gdamore/tcell/v2"
|
|
41
|
-
|
|
42
|
-
"github.com/gorce-ai/termwright/clients/go/annotate"
|
|
43
|
-
"github.com/gorce-ai/termwright/clients/go/protocol"
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
// probeName and probeVersion identify this probe in the handshake, distinctly
|
|
47
|
-
// from the hand-written adapter so a session can be told apart in diagnostics.
|
|
48
|
-
const (
|
|
49
|
-
probeName = "termwright-probe-tview"
|
|
50
|
-
probeVersion = "0.2.0"
|
|
51
|
-
frameworkVersion = "v0.42.0"
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
// termwrightProbeState is nil for an uninstrumented run, which is every run
|
|
55
|
-
// that does not carry the handshake variables.
|
|
56
|
-
type termwrightProbeState struct {
|
|
57
|
-
client *protocol.Client
|
|
58
|
-
|
|
59
|
-
mu sync.Mutex
|
|
60
|
-
ids map[Primitive]string
|
|
61
|
-
nextID int
|
|
62
|
-
dropped atomic.Uint64
|
|
63
|
-
timedOut atomic.Uint64
|
|
64
|
-
frames atomic.Uint64
|
|
65
|
-
redrawPending atomic.Bool
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// TermwrightProbeStats reports what the probe did and failed to do.
|
|
69
|
-
//
|
|
70
|
-
// Exported because the conformance fixture asserts on it: a drop counter no
|
|
71
|
-
// test can read is a drop counter nobody notices.
|
|
72
|
-
type TermwrightProbeStats struct {
|
|
73
|
-
Frames uint64
|
|
74
|
-
Dropped uint64
|
|
75
|
-
TimedOut uint64
|
|
76
|
-
FullSnapshotPending bool
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// TermwrightProbeStatistics returns the counters, or zeroes when dormant.
|
|
80
|
-
func TermwrightProbeStatistics() TermwrightProbeStats {
|
|
81
|
-
p := termwrightProbe
|
|
82
|
-
if p == nil {
|
|
83
|
-
return TermwrightProbeStats{}
|
|
84
|
-
}
|
|
85
|
-
return TermwrightProbeStats{
|
|
86
|
-
Frames: p.frames.Load(),
|
|
87
|
-
Dropped: p.dropped.Load(),
|
|
88
|
-
TimedOut: p.timedOut.Load(),
|
|
89
|
-
FullSnapshotPending: p.client.FullSnapshotRequired(),
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
var termwrightProbe = newTermwrightProbe()
|
|
94
|
-
|
|
95
|
-
// newTermwrightProbe honours the dormant rule: without an endpoint and a token
|
|
96
|
-
// the copy behaves exactly like upstream. `FromEnv` returns nil in that case,
|
|
97
|
-
// so there is one branch and no second source of truth about what "dormant"
|
|
98
|
-
// means.
|
|
99
|
-
func newTermwrightProbe() *termwrightProbeState {
|
|
100
|
-
client := protocol.FromEnv(protocol.Options{
|
|
101
|
-
AdapterName: probeName,
|
|
102
|
-
AdapterVersion: probeVersion,
|
|
103
|
-
Probe: &protocol.ProbeInfo{
|
|
104
|
-
Framework: "tview",
|
|
105
|
-
FrameworkVersion: frameworkVersion,
|
|
106
|
-
ProbeVersion: probeVersion,
|
|
107
|
-
IdentityKind: protocol.ProbeIdentityStable,
|
|
108
|
-
Capabilities: []protocol.ProbeCapability{
|
|
109
|
-
protocol.ProbeCapStableIdentity,
|
|
110
|
-
protocol.ProbeCapAnnotations,
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
// Bounds one frame write. The publish below happens under the
|
|
114
|
-
// application's write lock, so an unbounded write would freeze
|
|
115
|
-
// rendering whenever the driver stops reading — a frozen debugger, a
|
|
116
|
-
// slow consumer, a transport torn down mid-frame. A quarter of a
|
|
117
|
-
// second of not being read means the driver is not keeping up, and
|
|
118
|
-
// the next frame carries newer state anyway.
|
|
119
|
-
WriteTimeout: protocol.DefaultWriteTimeout,
|
|
120
|
-
})
|
|
121
|
-
if client == nil {
|
|
122
|
-
return nil
|
|
123
|
-
}
|
|
124
|
-
p := &termwrightProbeState{client: client, ids: make(map[Primitive]string)}
|
|
125
|
-
// The handshake must not block the first frame; publishing is a no-op
|
|
126
|
-
// until it completes.
|
|
127
|
-
go func() { _ = client.Start(protocol.DialTimeout) }()
|
|
128
|
-
return p
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// termwrightAfterFrame is called from draw(), after screen.Show() has flushed
|
|
132
|
-
// the frame's bytes.
|
|
133
|
-
func termwrightAfterFrame(a *Application, screen tcell.Screen) {
|
|
134
|
-
if p := termwrightProbe; p != nil {
|
|
135
|
-
p.afterFrame(a, screen)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// afterFrame is the method form, so a test can drive an instance of its own
|
|
140
|
-
// rather than the package-level probe a real run installs.
|
|
141
|
-
func (p *termwrightProbeState) afterFrame(a *Application, screen tcell.Screen) {
|
|
142
|
-
if screen == nil || a == nil {
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
if !p.client.Connected() {
|
|
146
|
-
p.redrawAfterHandshake(a)
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
columns, rows := screen.Size()
|
|
151
|
-
if columns <= 0 || rows <= 0 {
|
|
152
|
-
return
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
snapshot := p.snapshot(a, columns, rows, p.client.QualifiedObservations())
|
|
156
|
-
marker, err := p.client.Publish(snapshot)
|
|
157
|
-
if err != nil || marker == "" {
|
|
158
|
-
p.onPublishFailed(err)
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
// After the bytes, which is the whole reason this call site exists.
|
|
162
|
-
_, _ = os.Stdout.WriteString(marker)
|
|
163
|
-
p.frames.Add(1)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// redrawAfterHandshake closes the startup race for applications whose first
|
|
167
|
-
// frame is also their last frame until input arrives. The initial draw must not
|
|
168
|
-
// block on the driver, so the handshake remains asynchronous; once connected,
|
|
169
|
-
// one queued draw publishes the current tree without requiring synthetic user
|
|
170
|
-
// input. QueueUpdateDraw is the framework's supported cross-goroutine path.
|
|
171
|
-
func (p *termwrightProbeState) redrawAfterHandshake(a *Application) {
|
|
172
|
-
if !p.redrawPending.CompareAndSwap(false, true) {
|
|
173
|
-
return
|
|
174
|
-
}
|
|
175
|
-
go func() {
|
|
176
|
-
defer p.redrawPending.Store(false)
|
|
177
|
-
deadline := time.Now().Add(2 * time.Second)
|
|
178
|
-
for !p.client.Connected() && time.Now().Before(deadline) {
|
|
179
|
-
time.Sleep(5 * time.Millisecond)
|
|
180
|
-
}
|
|
181
|
-
if p.client.Connected() {
|
|
182
|
-
a.QueueUpdateDraw(func() {})
|
|
183
|
-
}
|
|
184
|
-
}()
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// onPublishFailed records a frame the driver will never see.
|
|
188
|
-
//
|
|
189
|
-
// Three things have to happen together, and leaving out any one of them
|
|
190
|
-
// produces a worse failure than dropping the frame did:
|
|
191
|
-
//
|
|
192
|
-
// 1. **No marker.** A marker names a revision; writing one for a tree that
|
|
193
|
-
// never arrived makes the driver wait for it and then report
|
|
194
|
-
// revision-expired — a diagnosis pointing at the adapter's timing rather
|
|
195
|
-
// than at the driver that stopped reading.
|
|
196
|
-
// 2. **A full snapshot next.** This probe has now lost part of its own fact
|
|
197
|
-
// stream, and a later delta would be based on a revision the driver never
|
|
198
|
-
// received. The producer obligation is to re-send the whole tree, and the
|
|
199
|
-
// client keeps the flag until a full tree is actually built.
|
|
200
|
-
// 3. **Keep rendering.** The application is mid-frame under its own lock;
|
|
201
|
-
// instrumentation failing is not the application failing.
|
|
202
|
-
func (p *termwrightProbeState) onPublishFailed(err error) {
|
|
203
|
-
p.dropped.Add(1)
|
|
204
|
-
p.client.RequireFullSnapshot()
|
|
205
|
-
|
|
206
|
-
if errors.Is(err, protocol.ErrWriteTimeout) {
|
|
207
|
-
// The stream now holds part of a frame and has no resynchronisation
|
|
208
|
-
// point, so the client closes the session. Nothing to retry: the next
|
|
209
|
-
// Publish returns immediately and the application draws on.
|
|
210
|
-
p.timedOut.Add(1)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// identity returns a stable id for a primitive.
|
|
215
|
-
//
|
|
216
|
-
// The pointer is the identity: tview retains its widget tree across frames, so
|
|
217
|
-
// the same *Button is the same button, and that is what makes the IR's
|
|
218
|
-
// `stable` identity kind honest here rather than a fabricated ordinal.
|
|
219
|
-
func (p *termwrightProbeState) identity(primitive Primitive) string {
|
|
220
|
-
p.mu.Lock()
|
|
221
|
-
defer p.mu.Unlock()
|
|
222
|
-
if id, ok := p.ids[primitive]; ok {
|
|
223
|
-
return id
|
|
224
|
-
}
|
|
225
|
-
p.nextID++
|
|
226
|
-
id := "n" + strconv.Itoa(p.nextID)
|
|
227
|
-
p.ids[primitive] = id
|
|
228
|
-
return id
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// snapshot walks the retained tree into the wire form.
|
|
232
|
-
func (p *termwrightProbeState) snapshot(a *Application, columns, rows int, qualified bool) *protocol.Snapshot {
|
|
233
|
-
var snapshot *protocol.Snapshot
|
|
234
|
-
if qualified {
|
|
235
|
-
snapshot = protocol.NewSnapshotV2("", 0, columns, rows)
|
|
236
|
-
snapshot.HitGrid = termwrightUnsupportedHitGrid()
|
|
237
|
-
} else {
|
|
238
|
-
snapshot = protocol.NewSnapshot("", 0, columns, rows)
|
|
239
|
-
}
|
|
240
|
-
if a.root == nil {
|
|
241
|
-
return snapshot
|
|
242
|
-
}
|
|
243
|
-
keys := make(map[annotate.SemanticKey]string)
|
|
244
|
-
duplicates := make(map[annotate.SemanticKey]struct{})
|
|
245
|
-
pending := make([]termwrightRelations, 0)
|
|
246
|
-
p.walk(a.root, "", false, columns, rows, qualified, snapshot, keys, duplicates, &pending)
|
|
247
|
-
maxRelations := protocol.DefaultLimits.MaxRelationTargets
|
|
248
|
-
if p.client != nil {
|
|
249
|
-
maxRelations = p.client.Limits().MaxRelationTargets
|
|
250
|
-
}
|
|
251
|
-
termwrightResolveRelations(snapshot, keys, duplicates, pending, maxRelations)
|
|
252
|
-
return snapshot
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// termwrightRelations holds author references until every node has been
|
|
256
|
-
// visited. A label may be drawn after the control it labels, so resolving in
|
|
257
|
-
// walk order would make declaration order part of the API.
|
|
258
|
-
type termwrightRelations struct {
|
|
259
|
-
nodeIndex int
|
|
260
|
-
labelledBy []annotate.SemanticKey
|
|
261
|
-
describedBy []annotate.SemanticKey
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// walk appends one node and recurses.
|
|
265
|
-
//
|
|
266
|
-
// `hidden` is inherited: a widget on an unshown page is not merely unfocused,
|
|
267
|
-
// it is not on screen, and every descendant of it is in the same position.
|
|
268
|
-
func (p *termwrightProbeState) walk(
|
|
269
|
-
primitive Primitive,
|
|
270
|
-
parentID string,
|
|
271
|
-
hidden bool,
|
|
272
|
-
columns, rows int,
|
|
273
|
-
qualified bool,
|
|
274
|
-
snapshot *protocol.Snapshot,
|
|
275
|
-
keys map[annotate.SemanticKey]string,
|
|
276
|
-
duplicates map[annotate.SemanticKey]struct{},
|
|
277
|
-
pending *[]termwrightRelations,
|
|
278
|
-
) {
|
|
279
|
-
if primitive == nil {
|
|
280
|
-
return
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
id := p.identity(primitive)
|
|
284
|
-
children := termwrightChildren(primitive)
|
|
285
|
-
|
|
286
|
-
// HasFocus reports true for ancestors of the focused primitive as well, so
|
|
287
|
-
// the flag belongs to the deepest one that claims it.
|
|
288
|
-
focused := !hidden && primitive.HasFocus() && !termwrightAnyFocus(children)
|
|
289
|
-
|
|
290
|
-
role := termwrightRole(primitive)
|
|
291
|
-
node := protocol.Node{
|
|
292
|
-
ID: id,
|
|
293
|
-
ParentID: parentID,
|
|
294
|
-
Role: role,
|
|
295
|
-
Name: termwrightName(primitive),
|
|
296
|
-
Value: termwrightValue(primitive),
|
|
297
|
-
State: termwrightState(primitive, focused, hidden),
|
|
298
|
-
P: protocol.ProvenanceFramework,
|
|
299
|
-
PX: map[string]string{
|
|
300
|
-
"role": protocol.ProvenanceRecognizer,
|
|
301
|
-
},
|
|
302
|
-
}
|
|
303
|
-
if qualified {
|
|
304
|
-
node.Geometry = termwrightGeometry(primitive, hidden, columns, rows)
|
|
305
|
-
} else {
|
|
306
|
-
node.Bounds = termwrightBounds(primitive, columns, rows)
|
|
307
|
-
}
|
|
308
|
-
// Required for a generic node, and useful on every other one: it is what
|
|
309
|
-
// keeps a widget this probe does not know about alive and identifiable
|
|
310
|
-
// rather than flattened into an anonymous region.
|
|
311
|
-
node.FrameworkType = termwrightTypeName(primitive)
|
|
312
|
-
// An author's annotation is merged on top of the observed facts, and only
|
|
313
|
-
// where the probe has nothing better: it may say what a widget *is*, never
|
|
314
|
-
// where it is or whether it has the focus. Those the probe measured.
|
|
315
|
-
meta, annotated := annotate.Lookup(primitive)
|
|
316
|
-
if annotated {
|
|
317
|
-
termwrightApplyAnnotation(meta, &node)
|
|
318
|
-
termwrightRegisterKey(meta.Key, id, keys, duplicates)
|
|
319
|
-
}
|
|
320
|
-
if parentID == "" {
|
|
321
|
-
snapshot.RootIDs = append(snapshot.RootIDs, id)
|
|
322
|
-
}
|
|
323
|
-
snapshot.Nodes = append(snapshot.Nodes, node)
|
|
324
|
-
if annotated && (len(meta.LabelledBy) > 0 || len(meta.DescribedBy) > 0) {
|
|
325
|
-
*pending = append(*pending, termwrightRelations{
|
|
326
|
-
nodeIndex: len(snapshot.Nodes) - 1,
|
|
327
|
-
labelledBy: meta.LabelledBy,
|
|
328
|
-
describedBy: meta.DescribedBy,
|
|
329
|
-
})
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
for _, child := range children {
|
|
333
|
-
p.walk(child.primitive, id, hidden || child.hidden, columns, rows, qualified, snapshot, keys, duplicates, pending)
|
|
334
|
-
}
|
|
335
|
-
p.appendSynthetic(primitive, id, hidden, qualified, snapshot)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
func termwrightRegisterKey(
|
|
339
|
-
key annotate.SemanticKey,
|
|
340
|
-
id string,
|
|
341
|
-
keys map[annotate.SemanticKey]string,
|
|
342
|
-
duplicates map[annotate.SemanticKey]struct{},
|
|
343
|
-
) {
|
|
344
|
-
if key == "" {
|
|
345
|
-
return
|
|
346
|
-
}
|
|
347
|
-
if _, duplicate := duplicates[key]; duplicate {
|
|
348
|
-
return
|
|
349
|
-
}
|
|
350
|
-
if previous, exists := keys[key]; exists && previous != id {
|
|
351
|
-
delete(keys, key)
|
|
352
|
-
duplicates[key] = struct{}{}
|
|
353
|
-
return
|
|
354
|
-
}
|
|
355
|
-
keys[key] = id
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
func termwrightResolveRelations(
|
|
359
|
-
snapshot *protocol.Snapshot,
|
|
360
|
-
keys map[annotate.SemanticKey]string,
|
|
361
|
-
duplicates map[annotate.SemanticKey]struct{},
|
|
362
|
-
pending []termwrightRelations,
|
|
363
|
-
maxRelations int,
|
|
364
|
-
) {
|
|
365
|
-
resolve := func(references []annotate.SemanticKey) []string {
|
|
366
|
-
resolved := make([]string, 0, len(references))
|
|
367
|
-
seen := make(map[string]struct{}, len(references))
|
|
368
|
-
for _, key := range references {
|
|
369
|
-
if len(resolved) >= maxRelations {
|
|
370
|
-
break
|
|
371
|
-
}
|
|
372
|
-
if _, duplicate := duplicates[key]; duplicate {
|
|
373
|
-
continue
|
|
374
|
-
}
|
|
375
|
-
id, found := keys[key]
|
|
376
|
-
if !found {
|
|
377
|
-
continue
|
|
378
|
-
}
|
|
379
|
-
if _, repeated := seen[id]; repeated {
|
|
380
|
-
continue
|
|
381
|
-
}
|
|
382
|
-
seen[id] = struct{}{}
|
|
383
|
-
resolved = append(resolved, id)
|
|
384
|
-
}
|
|
385
|
-
return resolved
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
for _, relation := range pending {
|
|
389
|
-
node := &snapshot.Nodes[relation.nodeIndex]
|
|
390
|
-
if ids := resolve(relation.labelledBy); len(ids) > 0 {
|
|
391
|
-
node.LabelledBy = ids
|
|
392
|
-
termwrightProvenance(node, "labelledBy", protocol.ProvenanceAnnotation)
|
|
393
|
-
}
|
|
394
|
-
if ids := resolve(relation.describedBy); len(ids) > 0 {
|
|
395
|
-
node.DescribedBy = ids
|
|
396
|
-
termwrightProvenance(node, "describedBy", protocol.ProvenanceAnnotation)
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
func termwrightProvenance(node *protocol.Node, field, source string) {
|
|
402
|
-
if node.PX == nil {
|
|
403
|
-
node.PX = make(map[string]string)
|
|
404
|
-
}
|
|
405
|
-
node.PX[field] = source
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// termwrightApplyAnnotation merges what the application declared.
|
|
409
|
-
//
|
|
410
|
-
// tview retains its widgets, so a registry keyed by the primitive's identity
|
|
411
|
-
// works here — which is why tview annotates by registration while Charm, whose
|
|
412
|
-
// components are copied values, annotates through an interface.
|
|
413
|
-
func termwrightApplyAnnotation(meta annotate.Semantics, node *protocol.Node) {
|
|
414
|
-
if meta.Role != "" {
|
|
415
|
-
// Validated against the closed set and dropped when unknown, rather
|
|
416
|
-
// than guessed at: exhaustive switches downstream depend on that set
|
|
417
|
-
// staying closed, and a typo in an annotation is the author's to fix.
|
|
418
|
-
if role := protocol.Role(meta.Role); protocol.ValidRole(role) {
|
|
419
|
-
node.Role = role
|
|
420
|
-
termwrightProvenance(node, "role", protocol.ProvenanceAnnotation)
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
if meta.Name != "" {
|
|
424
|
-
node.Name = meta.Name
|
|
425
|
-
termwrightProvenance(node, "name", protocol.ProvenanceAnnotation)
|
|
426
|
-
}
|
|
427
|
-
if meta.TestID != "" {
|
|
428
|
-
node.TestID = meta.TestID
|
|
429
|
-
termwrightProvenance(node, "testId", protocol.ProvenanceAnnotation)
|
|
430
|
-
}
|
|
431
|
-
if meta.Description != "" {
|
|
432
|
-
node.Description = meta.Description
|
|
433
|
-
termwrightProvenance(node, "description", protocol.ProvenanceAnnotation)
|
|
434
|
-
}
|
|
435
|
-
// Domain state has its own namespace, so it cannot pollute the closed
|
|
436
|
-
// portable state vocabulary or masquerade as prose.
|
|
437
|
-
if len(meta.Domain) > 0 {
|
|
438
|
-
node.Extended = make(map[string]any, len(meta.Domain))
|
|
439
|
-
for key, value := range meta.Domain {
|
|
440
|
-
node.Extended[key] = value
|
|
441
|
-
}
|
|
442
|
-
termwrightProvenance(node, "extended", protocol.ProvenanceAnnotation)
|
|
443
|
-
}
|
|
444
|
-
seenActions := make(map[protocol.Action]struct{}, len(meta.Actions))
|
|
445
|
-
for _, action := range meta.Actions {
|
|
446
|
-
if !protocol.ValidAction(action) {
|
|
447
|
-
continue
|
|
448
|
-
}
|
|
449
|
-
if _, duplicate := seenActions[action]; duplicate {
|
|
450
|
-
continue
|
|
451
|
-
}
|
|
452
|
-
seenActions[action] = struct{}{}
|
|
453
|
-
node.Actions = append(node.Actions, action)
|
|
454
|
-
}
|
|
455
|
-
if len(node.Actions) > 0 {
|
|
456
|
-
termwrightProvenance(node, "actions", protocol.ProvenanceAnnotation)
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// termwrightChild is a child plus whether its container is showing it.
|
|
461
|
-
type termwrightChild struct {
|
|
462
|
-
primitive Primitive
|
|
463
|
-
hidden bool
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// termwrightChildren enumerates a container's children from inside the package.
|
|
467
|
-
func termwrightChildren(p Primitive) []termwrightChild {
|
|
468
|
-
switch c := p.(type) {
|
|
469
|
-
case *Flex:
|
|
470
|
-
children := make([]termwrightChild, 0, len(c.items))
|
|
471
|
-
for _, item := range c.items {
|
|
472
|
-
if item.Item != nil {
|
|
473
|
-
children = append(children, termwrightChild{primitive: item.Item})
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return children
|
|
477
|
-
case *Grid:
|
|
478
|
-
// The case an out-of-package adapter cannot serve at all. `visible`
|
|
479
|
-
// carries the last draw's decision, which is exactly what a test means
|
|
480
|
-
// by "is it on screen".
|
|
481
|
-
children := make([]termwrightChild, 0, len(c.items))
|
|
482
|
-
for _, item := range c.items {
|
|
483
|
-
if item.Item != nil {
|
|
484
|
-
children = append(children, termwrightChild{primitive: item.Item, hidden: !item.visible})
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return children
|
|
488
|
-
case *Pages:
|
|
489
|
-
children := make([]termwrightChild, 0, len(c.pages))
|
|
490
|
-
for _, page := range c.pages {
|
|
491
|
-
if page.Item != nil {
|
|
492
|
-
children = append(children, termwrightChild{primitive: page.Item, hidden: !page.Visible})
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
return children
|
|
496
|
-
case *Frame:
|
|
497
|
-
if c.primitive != nil {
|
|
498
|
-
return []termwrightChild{{primitive: c.primitive}}
|
|
499
|
-
}
|
|
500
|
-
case *Form:
|
|
501
|
-
children := make([]termwrightChild, 0, len(c.items)+len(c.buttons))
|
|
502
|
-
for _, item := range c.items {
|
|
503
|
-
children = append(children, termwrightChild{primitive: item})
|
|
504
|
-
}
|
|
505
|
-
for _, button := range c.buttons {
|
|
506
|
-
children = append(children, termwrightChild{primitive: button})
|
|
507
|
-
}
|
|
508
|
-
return children
|
|
509
|
-
case *Modal:
|
|
510
|
-
if c.frame != nil {
|
|
511
|
-
return []termwrightChild{{primitive: c.frame}}
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return nil
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
func termwrightAnyFocus(children []termwrightChild) bool {
|
|
518
|
-
for _, child := range children {
|
|
519
|
-
if child.primitive != nil && child.primitive.HasFocus() {
|
|
520
|
-
return true
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
return false
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// termwrightRole maps a widget type to the closed role set.
|
|
527
|
-
//
|
|
528
|
-
// Deliberately identical to the hand-written adapter's mapping: the two must
|
|
529
|
-
// agree, or the same application would describe itself differently depending
|
|
530
|
-
// on how it was instrumented, and every conformance snapshot would fork.
|
|
531
|
-
func termwrightRole(p Primitive) protocol.Role {
|
|
532
|
-
switch p.(type) {
|
|
533
|
-
case *Button:
|
|
534
|
-
return protocol.RoleButton
|
|
535
|
-
case *Checkbox:
|
|
536
|
-
return protocol.RoleCheckbox
|
|
537
|
-
case *InputField, *TextArea:
|
|
538
|
-
return protocol.RoleTextbox
|
|
539
|
-
case *DropDown, *List, *TreeView:
|
|
540
|
-
return protocol.RoleList
|
|
541
|
-
case *Table:
|
|
542
|
-
return protocol.RoleTable
|
|
543
|
-
case *TextView:
|
|
544
|
-
return protocol.RoleText
|
|
545
|
-
case *Modal:
|
|
546
|
-
return protocol.RoleDialog
|
|
547
|
-
case *Form, *Flex, *Grid, *Pages, *Frame, *Box:
|
|
548
|
-
return protocol.RoleRegion
|
|
549
|
-
}
|
|
550
|
-
// Never dropped: an unrecognised widget keeps its bounds, its children and
|
|
551
|
-
// its own type name, which is what makes a new tview release degrade
|
|
552
|
-
// rather than disappear.
|
|
553
|
-
return protocol.RoleGeneric
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// termwrightName derives the accessible name.
|
|
557
|
-
func termwrightName(p Primitive) string {
|
|
558
|
-
switch widget := p.(type) {
|
|
559
|
-
case *Button:
|
|
560
|
-
return widget.GetLabel()
|
|
561
|
-
case *Checkbox:
|
|
562
|
-
return termwrightFirst(widget.GetLabel(), widget.GetTitle())
|
|
563
|
-
case *InputField:
|
|
564
|
-
return termwrightFirst(widget.GetLabel(), widget.GetTitle())
|
|
565
|
-
case *DropDown:
|
|
566
|
-
return termwrightFirst(widget.GetLabel(), widget.GetTitle())
|
|
567
|
-
case *TextArea:
|
|
568
|
-
return termwrightFirst(widget.GetLabel(), widget.GetTitle())
|
|
569
|
-
case *TextView:
|
|
570
|
-
return termwrightFirst(widget.GetTitle(), termwrightTrim(widget.GetText(true)))
|
|
571
|
-
case *Modal:
|
|
572
|
-
// Modal exposes no getter at all; the text is the only name it has.
|
|
573
|
-
return termwrightTrim(widget.text)
|
|
574
|
-
case *Box:
|
|
575
|
-
return widget.GetTitle()
|
|
576
|
-
}
|
|
577
|
-
if boxed, ok := p.(interface{ GetTitle() string }); ok {
|
|
578
|
-
return boxed.GetTitle()
|
|
579
|
-
}
|
|
580
|
-
return ""
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// termwrightValue reports the current value of a value-bearing widget.
|
|
584
|
-
//
|
|
585
|
-
// A pointer because the empty string is a fact: `""` says the field is empty,
|
|
586
|
-
// absent says this widget carries no value at all. Collapsing the two would
|
|
587
|
-
// make an assertion on an emptied input box unwritable.
|
|
588
|
-
func termwrightValue(p Primitive) *string {
|
|
589
|
-
switch widget := p.(type) {
|
|
590
|
-
case *InputField:
|
|
591
|
-
text := widget.GetText()
|
|
592
|
-
return &text
|
|
593
|
-
case *TextArea:
|
|
594
|
-
text := widget.GetText()
|
|
595
|
-
return &text
|
|
596
|
-
case *DropDown:
|
|
597
|
-
_, text := widget.GetCurrentOption()
|
|
598
|
-
return &text
|
|
599
|
-
}
|
|
600
|
-
return nil
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// termwrightTypeName is the framework's own name for the widget, without the
|
|
604
|
-
// package qualifier that would be identical on every node.
|
|
605
|
-
func termwrightTypeName(p Primitive) string {
|
|
606
|
-
name := reflect.TypeOf(p).String()
|
|
607
|
-
if index := strings.LastIndex(name, "."); index >= 0 {
|
|
608
|
-
name = name[index+1:]
|
|
609
|
-
}
|
|
610
|
-
return name
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// termwrightBounds reports where the widget was drawn.
|
|
614
|
-
//
|
|
615
|
-
// This is the IR's `intendedRect`: what the parent assigned, not a claim on
|
|
616
|
-
// cells. tview computes no clip, so `visibleRect` is genuinely unobservable
|
|
617
|
-
// here and is not invented — a widget scrolled out of a Grid still reports the
|
|
618
|
-
// rectangle it was given.
|
|
619
|
-
func termwrightBounds(p Primitive, columns, rows int) *protocol.Rect {
|
|
620
|
-
x, y, width, height := p.GetRect()
|
|
621
|
-
if width <= 0 || height <= 0 {
|
|
622
|
-
return nil
|
|
623
|
-
}
|
|
624
|
-
if x >= columns || y >= rows {
|
|
625
|
-
return nil
|
|
626
|
-
}
|
|
627
|
-
return &protocol.Rect{Row: y, Column: x, Width: width, Height: height}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// termwrightGeometry qualifies only facts the retained tview tree exposes.
|
|
631
|
-
// GetRect is the parent's intended allocation. The framework exposes no
|
|
632
|
-
// general nested clipping or paint ownership, so visibleRect is limited to the
|
|
633
|
-
// exact viewport intersection and pointer hit testing remains unsupported.
|
|
634
|
-
func termwrightGeometry(p Primitive, hidden bool, columns, rows int) *protocol.NodeGeometryObservations {
|
|
635
|
-
displayed := !hidden
|
|
636
|
-
geometry := &protocol.NodeGeometryObservations{
|
|
637
|
-
Displayed: protocol.Observation[bool]{Status: "known", Value: &displayed, Evidence: "probe"},
|
|
638
|
-
}
|
|
639
|
-
if hidden {
|
|
640
|
-
geometry.IntendedRect = protocol.Observation[protocol.Rect]{Status: "absent", Reason: "not-displayed"}
|
|
641
|
-
geometry.VisibleRect = protocol.Observation[protocol.Rect]{Status: "absent", Reason: "not-displayed"}
|
|
642
|
-
return geometry
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
x, y, width, height := p.GetRect()
|
|
646
|
-
if width <= 0 || height <= 0 {
|
|
647
|
-
geometry.IntendedRect = protocol.Observation[protocol.Rect]{Status: "absent", Reason: "not-laid-out"}
|
|
648
|
-
geometry.VisibleRect = protocol.Observation[protocol.Rect]{Status: "absent", Reason: "not-laid-out"}
|
|
649
|
-
return geometry
|
|
650
|
-
}
|
|
651
|
-
intended := protocol.Rect{Row: y, Column: x, Width: width, Height: height}
|
|
652
|
-
visible := termwrightViewportIntersection(intended, columns, rows)
|
|
653
|
-
geometry.IntendedRect = protocol.Observation[protocol.Rect]{Status: "known", Value: &intended, Evidence: "probe"}
|
|
654
|
-
geometry.VisibleRect = protocol.Observation[protocol.Rect]{Status: "known", Value: &visible, Evidence: "viewport-clip"}
|
|
655
|
-
return geometry
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
func termwrightViewportIntersection(rect protocol.Rect, columns, rows int) protocol.Rect {
|
|
659
|
-
left := termwrightMax(0, termwrightMin(rect.Column, columns))
|
|
660
|
-
top := termwrightMax(0, termwrightMin(rect.Row, rows))
|
|
661
|
-
right := termwrightMax(left, termwrightMin(rect.Column+rect.Width, columns))
|
|
662
|
-
bottom := termwrightMax(top, termwrightMin(rect.Row+rect.Height, rows))
|
|
663
|
-
return protocol.Rect{Row: top, Column: left, Width: right - left, Height: bottom - top}
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
func termwrightMin(left, right int) int {
|
|
667
|
-
if left < right {
|
|
668
|
-
return left
|
|
669
|
-
}
|
|
670
|
-
return right
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
func termwrightMax(left, right int) int {
|
|
674
|
-
if left > right {
|
|
675
|
-
return left
|
|
676
|
-
}
|
|
677
|
-
return right
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
func termwrightUnsupportedHitGrid() *protocol.Observation[protocol.PointerHitGrid] {
|
|
681
|
-
return &protocol.Observation[protocol.PointerHitGrid]{
|
|
682
|
-
Status: "unsupported", Capability: "pointer-hit-grid", Reason: "framework-unobservable",
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// termwrightScroll reports a scroll offset only when it is a fact.
|
|
687
|
-
//
|
|
688
|
-
// Several of these fields are meaningless until the widget has been drawn once
|
|
689
|
-
// (the audit lists them: TextView.pageSize, TreeView.nodes, Table.visibleRows
|
|
690
|
-
// and friends), and tview leaves some of them negative until then. A negative
|
|
691
|
-
// offset is not "scrolled backwards", it is "not decided yet" — publishing it
|
|
692
|
-
// asserts something false and, since the schema requires a non-negative
|
|
693
|
-
// integer, gets the whole snapshot refused.
|
|
694
|
-
func termwrightScroll(offset int) *int {
|
|
695
|
-
if offset < 0 {
|
|
696
|
-
return nil
|
|
697
|
-
}
|
|
698
|
-
return protocol.Int(offset)
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
// termwrightCount is the same guard for set sizes.
|
|
702
|
-
func termwrightCount(count int) *int {
|
|
703
|
-
if count < 0 {
|
|
704
|
-
return nil
|
|
705
|
-
}
|
|
706
|
-
return protocol.Int(count)
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// termwrightState reads the observable state of one widget.
|
|
710
|
-
func termwrightState(p Primitive, focused, hidden bool) *protocol.State {
|
|
711
|
-
state := protocol.State{}
|
|
712
|
-
empty := true
|
|
713
|
-
|
|
714
|
-
if focused {
|
|
715
|
-
state.Focused = protocol.Bool(true)
|
|
716
|
-
empty = false
|
|
717
|
-
}
|
|
718
|
-
if hidden {
|
|
719
|
-
state.Hidden = protocol.Bool(true)
|
|
720
|
-
empty = false
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
switch widget := p.(type) {
|
|
724
|
-
case *Button:
|
|
725
|
-
if widget.IsDisabled() {
|
|
726
|
-
state.Disabled = protocol.Bool(true)
|
|
727
|
-
empty = false
|
|
728
|
-
}
|
|
729
|
-
case *Checkbox:
|
|
730
|
-
state.Checked = widget.IsChecked()
|
|
731
|
-
if widget.disabled {
|
|
732
|
-
state.Disabled = protocol.Bool(true)
|
|
733
|
-
}
|
|
734
|
-
empty = false
|
|
735
|
-
case *DropDown:
|
|
736
|
-
if widget.disabled {
|
|
737
|
-
state.Disabled = protocol.Bool(true)
|
|
738
|
-
empty = false
|
|
739
|
-
}
|
|
740
|
-
state.SetSize = termwrightCount(widget.GetOptionCount())
|
|
741
|
-
state.Expanded = protocol.Bool(widget.IsOpen())
|
|
742
|
-
empty = false
|
|
743
|
-
case *TextArea:
|
|
744
|
-
if widget.GetDisabled() {
|
|
745
|
-
state.Disabled = protocol.Bool(true)
|
|
746
|
-
}
|
|
747
|
-
row, _ := widget.GetOffset()
|
|
748
|
-
state.ScrollOffset = termwrightScroll(row)
|
|
749
|
-
empty = false
|
|
750
|
-
case *List:
|
|
751
|
-
state.SetSize = termwrightCount(widget.GetItemCount())
|
|
752
|
-
// Named `itemOffset` here and `lineOffset`, `rowOffset` or `offsetY`
|
|
753
|
-
// on the other four scrollables; there is no single field to reach for.
|
|
754
|
-
offset, _ := widget.GetOffset()
|
|
755
|
-
state.ScrollOffset = termwrightScroll(offset)
|
|
756
|
-
empty = false
|
|
757
|
-
case *Table:
|
|
758
|
-
state.SetSize = termwrightCount(widget.GetRowCount())
|
|
759
|
-
row, _ := widget.GetOffset()
|
|
760
|
-
state.ScrollOffset = termwrightScroll(row)
|
|
761
|
-
empty = false
|
|
762
|
-
case *TextView:
|
|
763
|
-
row, _ := widget.GetScrollOffset()
|
|
764
|
-
state.ScrollOffset = termwrightScroll(row)
|
|
765
|
-
empty = false
|
|
766
|
-
case *TreeView:
|
|
767
|
-
state.ScrollOffset = termwrightScroll(widget.GetScrollOffset())
|
|
768
|
-
state.SetSize = termwrightCount(widget.GetRowCount())
|
|
769
|
-
empty = false
|
|
770
|
-
case *Modal:
|
|
771
|
-
state.Modal = protocol.Bool(true)
|
|
772
|
-
empty = false
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
if empty {
|
|
776
|
-
return nil
|
|
777
|
-
}
|
|
778
|
-
return &state
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// appendSynthetic emits nodes for entries that are not primitives of their own
|
|
782
|
-
// — list items and dropdown options — so they are addressable by role and name.
|
|
783
|
-
// They carry no bounds, which the schema allows.
|
|
784
|
-
func (p *termwrightProbeState) appendSynthetic(
|
|
785
|
-
primitive Primitive,
|
|
786
|
-
parentID string,
|
|
787
|
-
hidden bool,
|
|
788
|
-
qualified bool,
|
|
789
|
-
snapshot *protocol.Snapshot,
|
|
790
|
-
) {
|
|
791
|
-
switch widget := primitive.(type) {
|
|
792
|
-
case *List:
|
|
793
|
-
current := widget.GetCurrentItem()
|
|
794
|
-
count := widget.GetItemCount()
|
|
795
|
-
for index := 0; index < count; index++ {
|
|
796
|
-
main, secondary := widget.GetItemText(index)
|
|
797
|
-
node := protocol.Node{
|
|
798
|
-
ID: parentID + ":item" + strconv.Itoa(index),
|
|
799
|
-
ParentID: parentID,
|
|
800
|
-
Role: protocol.RoleListItem,
|
|
801
|
-
Name: termwrightFirst(main, secondary),
|
|
802
|
-
State: termwrightItemState(index == current, index, count, hidden),
|
|
803
|
-
P: protocol.ProvenanceFramework,
|
|
804
|
-
PX: map[string]string{
|
|
805
|
-
"role": protocol.ProvenanceRecognizer,
|
|
806
|
-
},
|
|
807
|
-
}
|
|
808
|
-
termwrightSyntheticGeometry(&node, qualified)
|
|
809
|
-
snapshot.Nodes = append(snapshot.Nodes, node)
|
|
810
|
-
}
|
|
811
|
-
case *DropDown:
|
|
812
|
-
current, _ := widget.GetCurrentOption()
|
|
813
|
-
count := widget.GetOptionCount()
|
|
814
|
-
for index := 0; index < count; index++ {
|
|
815
|
-
node := protocol.Node{
|
|
816
|
-
ID: parentID + ":option" + strconv.Itoa(index),
|
|
817
|
-
ParentID: parentID,
|
|
818
|
-
Role: protocol.RoleListItem,
|
|
819
|
-
Name: widget.options[index].Text,
|
|
820
|
-
State: termwrightItemState(index == current, index, count, hidden),
|
|
821
|
-
P: protocol.ProvenanceFramework,
|
|
822
|
-
PX: map[string]string{
|
|
823
|
-
"role": protocol.ProvenanceRecognizer,
|
|
824
|
-
},
|
|
825
|
-
}
|
|
826
|
-
termwrightSyntheticGeometry(&node, qualified)
|
|
827
|
-
snapshot.Nodes = append(snapshot.Nodes, node)
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
func termwrightSyntheticGeometry(node *protocol.Node, qualified bool) {
|
|
833
|
-
if !qualified {
|
|
834
|
-
return
|
|
835
|
-
}
|
|
836
|
-
node.Geometry = &protocol.NodeGeometryObservations{
|
|
837
|
-
Displayed: protocol.Observation[bool]{Status: "unknown", Reason: "not-reported"},
|
|
838
|
-
IntendedRect: protocol.Observation[protocol.Rect]{Status: "unknown", Reason: "not-reported"},
|
|
839
|
-
VisibleRect: protocol.Observation[protocol.Rect]{Status: "unknown", Reason: "clip-unobservable"},
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
func termwrightItemState(selected bool, index, count int, hidden bool) *protocol.State {
|
|
844
|
-
state := protocol.State{
|
|
845
|
-
Selected: protocol.Bool(selected),
|
|
846
|
-
PositionInSet: protocol.Int(index + 1),
|
|
847
|
-
SetSize: protocol.Int(count),
|
|
848
|
-
}
|
|
849
|
-
if hidden {
|
|
850
|
-
state.Hidden = protocol.Bool(true)
|
|
851
|
-
}
|
|
852
|
-
return &state
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
func termwrightFirst(candidates ...string) string {
|
|
856
|
-
for _, candidate := range candidates {
|
|
857
|
-
if trimmed := termwrightTrim(candidate); trimmed != "" {
|
|
858
|
-
return trimmed
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
return ""
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// termwrightTrim collapses the padding widgets use for layout, so a name reads
|
|
865
|
-
// the way it looks rather than the way it was spaced.
|
|
866
|
-
func termwrightTrim(text string) string {
|
|
867
|
-
start := 0
|
|
868
|
-
end := len(text)
|
|
869
|
-
for start < end && (text[start] == ' ' || text[start] == '\t' || text[start] == '\n' || text[start] == '\r') {
|
|
870
|
-
start++
|
|
871
|
-
}
|
|
872
|
-
for end > start && (text[end-1] == ' ' || text[end-1] == '\t' || text[end-1] == '\n' || text[end-1] == '\r') {
|
|
873
|
-
end--
|
|
874
|
-
}
|
|
875
|
-
return text[start:end]
|
|
876
|
-
}
|