-
Notifications
You must be signed in to change notification settings - Fork 389
/
ui.go
4412 lines (3825 loc) · 127 KB
/
ui.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source
// code is governed by the MIT license that can be found in the LICENSE
// file.
// Package ui contains user-interface functions and helpers for termshark.
package ui
import (
"encoding/xml"
"fmt"
"math"
"os"
"reflect"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/gcla/deep"
"github.com/gcla/gowid"
"github.com/gcla/gowid/gwutil"
"github.com/gcla/gowid/widgets/button"
"github.com/gcla/gowid/widgets/clicktracker"
"github.com/gcla/gowid/widgets/columns"
"github.com/gcla/gowid/widgets/dialog"
"github.com/gcla/gowid/widgets/disable"
"github.com/gcla/gowid/widgets/divider"
"github.com/gcla/gowid/widgets/fill"
"github.com/gcla/gowid/widgets/framed"
"github.com/gcla/gowid/widgets/holder"
"github.com/gcla/gowid/widgets/hpadding"
"github.com/gcla/gowid/widgets/isselected"
"github.com/gcla/gowid/widgets/list"
"github.com/gcla/gowid/widgets/menu"
"github.com/gcla/gowid/widgets/null"
"github.com/gcla/gowid/widgets/overlay"
"github.com/gcla/gowid/widgets/pile"
"github.com/gcla/gowid/widgets/progress"
"github.com/gcla/gowid/widgets/selectable"
"github.com/gcla/gowid/widgets/spinner"
"github.com/gcla/gowid/widgets/styled"
"github.com/gcla/gowid/widgets/table"
"github.com/gcla/gowid/widgets/text"
"github.com/gcla/gowid/widgets/tree"
"github.com/gcla/gowid/widgets/vpadding"
"github.com/gcla/termshark/v2"
"github.com/gcla/termshark/v2/configs/profiles"
"github.com/gcla/termshark/v2/pkg/fields"
"github.com/gcla/termshark/v2/pkg/noroot"
"github.com/gcla/termshark/v2/pkg/pcap"
"github.com/gcla/termshark/v2/pkg/pdmltree"
"github.com/gcla/termshark/v2/pkg/psmlmodel"
"github.com/gcla/termshark/v2/pkg/shark"
"github.com/gcla/termshark/v2/pkg/system"
"github.com/gcla/termshark/v2/pkg/theme"
"github.com/gcla/termshark/v2/ui/menuutil"
"github.com/gcla/termshark/v2/ui/tableutil"
"github.com/gcla/termshark/v2/widgets"
"github.com/gcla/termshark/v2/widgets/appkeys"
"github.com/gcla/termshark/v2/widgets/copymodetree"
"github.com/gcla/termshark/v2/widgets/enableselected"
"github.com/gcla/termshark/v2/widgets/expander"
"github.com/gcla/termshark/v2/widgets/filter"
"github.com/gcla/termshark/v2/widgets/hexdumper2"
"github.com/gcla/termshark/v2/widgets/ifwidget"
"github.com/gcla/termshark/v2/widgets/mapkeys"
"github.com/gcla/termshark/v2/widgets/minibuffer"
"github.com/gcla/termshark/v2/widgets/resizable"
"github.com/gcla/termshark/v2/widgets/rossshark"
"github.com/gcla/termshark/v2/widgets/search"
"github.com/gcla/termshark/v2/widgets/withscrollbar"
"github.com/gdamore/tcell/v2"
lru "github.com/hashicorp/golang-lru"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
//======================================================================
var Goroutinewg *sync.WaitGroup
type WidgetOwner int
const (
NoOwner WidgetOwner = iota
LoaderOwns
SearchOwns
)
// Global so that we can change the displayed packet in the struct view, etc
// test
var appViewNoKeys *holder.Widget
var appView *holder.Widget
var mbView *holder.Widget
var mainViewNoKeys *holder.Widget
var mainView *appkeys.KeyWidget
var pleaseWaitSpinner *spinner.Widget
var mainviewRows *resizable.PileWidget
var mainview gowid.IWidget
var altview1 gowid.IWidget
var altview1OuterRows *resizable.PileWidget
var altview1Pile *resizable.PileWidget
var altview1Cols *resizable.ColumnsWidget
var altview2 gowid.IWidget
var altview2OuterRows *resizable.PileWidget
var altview2Pile *resizable.PileWidget
var altview2Cols *resizable.ColumnsWidget
var viewOnlyPacketList *pile.Widget
var viewOnlyPacketStructure *pile.Widget
var viewOnlyPacketHex *pile.Widget
var filterCols *columns.Widget
var loadProg *columns.Widget
var loadStop *button.Widget
var searchProg *columns.Widget
var searchStop *button.Widget
var progWidgetIdx int
var mainviewPaths [][]interface{}
var altview1Paths [][]interface{}
var altview2Paths [][]interface{}
var maxViewPath []interface{}
var filterPathMain []interface{}
var filterPathAlt []interface{}
var filterPathMax []interface{}
var searchPathMain []interface{}
var searchPathAlt []interface{}
var searchPathMax []interface{}
var menuPathMain []interface{}
var menuPathAlt []interface{}
var menuPathMax []interface{}
var view1idx int
var view2idx int
var generalMenu *menu.Widget
var analysisMenu *menu.Widget
var savedMenu *menu.Widget
var profileMenu *menu.Widget
var FilterWidget *filter.Widget
var Fin *rossshark.Widget
var CopyModeWidget gowid.IWidget
var CopyModePredicate ifwidget.Predicate
var openMenuSite *menu.SiteWidget
var openAnalysisSite *menu.SiteWidget
var packetListViewHolder *holder.Widget
var packetListTable *table.BoundedWidget
var packetStructureViewHolder *holder.Widget
var packetHexViewHolder *holder.Widget
var progressHolder *holder.Widget
var progressOwner WidgetOwner
var stopCurrentSearch search.IRequestStop
var loadProgress *progress.Widget
var loadSpinner *spinner.Widget
var savedListBoxWidgetHolder *holder.Widget
var singlePacketViewMsgHolder *holder.Widget // either empty or "loading..."
var keyMapper *mapkeys.Widget
// For deconstructing the @showname PDML attribute into a short form for the UI
var shownameRe = regexp.MustCompile(`(.*?= )?([^:]+)`)
type MenuHolder struct {
gowid.IMenuCompatible
}
var multiMenu *MenuHolder = &MenuHolder{}
var multiMenuWidget *holder.Widget
var multiMenu2 *MenuHolder = &MenuHolder{}
var multiMenu2Widget *holder.Widget
var multiMenu1Opener MultiMenuOpener
var multiMenu2Opener MultiMenuOpener
var tabViewsForward map[gowid.IWidget]gowid.IWidget
var tabViewsBackward map[gowid.IWidget]gowid.IWidget
var currentProfile *text.Widget
var currentProfileWidget *columns.Widget
var currentProfileWidgetHolder *holder.Widget
var openProfileSite *menu.SiteWidget
var currentCapture *text.Widget
var currentCaptureWidget *columns.Widget
var currentCaptureWidgetHolder *holder.Widget
var nullw *null.Widget // empty
var fillSpace *fill.Widget
var fillVBar *fill.Widget
var colSpace *gowid.ContainerWidget
var curPacketStructWidget *copymodetree.Widget
var packetHexWidgets *lru.Cache
var packetListView *psmlTableRowWidget
// Usually false. When the user moves the cursor in the hex widget, a callback will update the
// struct widget's current expansion. That results in a callback to the current hex widget to
// update its position - ad inf. The hex widget callback checks to see whether or not the hex
// widget has "focus". If it doesn't, the callback is suppressed - to short circuit the callback
// loop. BUT - after a packet search, we reposition the hex widget and want the callback from
// hex to struct to happen once. So this is a workaround to allow it in that case.
//
// This variable has two effects:
// - when the hex widget is positioned programmatically, and focus is not on the hex widget,
// the struct widget is nevertheless updated accordingly
// - but when the struct widget is updated, if the innermost layer does not capture the
// current hex location (the search destination), DON'T update the hex position to be
// inside the PDML's innermost layer, which maybe somewhere else in the packet.
//
var allowHexToStructRepositioning bool
var filterWithSearch gowid.IWidget
var filterWithoutSearch gowid.IWidget
var filterHolder *holder.Widget
var SearchWidget *search.Widget
var Loadingw gowid.IWidget // "loading..."
var MissingMsgw gowid.IWidget // centered, holding singlePacketViewMsgHolder
var EmptyStructViewTimer *time.Timer
var EmptyHexViewTimer *time.Timer
var curSearchPosition tree.IPos // e.g. [0, 4] -> the indices of the struct layer
var curExpandedStructNodes pdmltree.ExpandedPaths // a path to each expanded node in the packet, preserved while navigating
var curStructPosition tree.IPos // e.g. [0, 2, 1] -> the indices of the expanded nodes
var curPdmlPosition []string // e.g. [ , tcp, tcp.srcport ] -> the path from focus to root in the current struct
var curStructWidgetState interface{} // e.g. {linesFromTop: 1, ...} -> the positioning of the current struct widget
var curColumnFilter string // e.g. tcp.port - updated as the user moves through the struct widget
var curColumnFilterName string // e.g. "TCP port" - from the showname attribute in the PDML
var curColumnFilterValue string // e.g. "80" - from the show attribute
var CacheRequests []pcap.LoadPcapSlice
var CacheRequestsChan chan struct{} // false means started, true means finished
var QuitRequestedChan chan struct{}
var StartUIChan chan struct{}
var StartUIOnce sync.Once
// Store this for vim-like keypresses that are a sequence e.g. "ZZ"
var keyState termshark.KeyState
var marksMap map[rune]termshark.JumpPos
var globalMarksMap map[rune]termshark.GlobalJumpPos
var lastJumpPos int
var NoGlobalJump termshark.GlobalJumpPos // leave as default, like a placeholder
var Loader *pcap.PacketLoader
var FieldCompleter *fields.TSharkFields // share this - safe once constructed
var WriteToSelected bool // true if the user provided the -w flag
var WriteToDeleted bool // true if the user deleted the temporary pcap before quitting
var DarkMode bool // global state in app
var PacketColors bool // global state in app
var PacketColorsSupported bool // global state in app - true if it's even possible
var AutoScroll bool // true if the packet list should auto-scroll when listening on an interface.
var newPacketsArrived bool // true if current updates are due to new packets when listening on an interface.
var reenableAutoScroll bool // set to true by keypress processing widgets - used with newPacketsArrived
var Running bool // true if gowid/tcell is controlling the terminal
var QuitRequested bool // true if a quit has been issued, but not yet processed. Stops some handlers displaying errors.
//======================================================================
func init() {
curExpandedStructNodes = make(pdmltree.ExpandedPaths, 0, 20)
QuitRequestedChan = make(chan struct{}, 1) // buffered because send happens from ui goroutine, which runs global select
CacheRequestsChan = make(chan struct{}, 1000)
CacheRequests = make([]pcap.LoadPcapSlice, 0)
// Buffered because I might send something in this goroutine
StartUIChan = make(chan struct{}, 1)
keyState.NumberPrefix = -1 // 0 might be meaningful
marksMap = make(map[rune]termshark.JumpPos)
globalMarksMap = make(map[rune]termshark.GlobalJumpPos)
lastJumpPos = -1
EnsureTemplateData()
TemplateData["Marks"] = marksMap
TemplateData["GlobalMarks"] = globalMarksMap
TemplateData["Maps"] = getMappings{}
}
type globalJump struct {
file string
pos int
}
type getMappings struct{}
func (g getMappings) Get() []termshark.KeyMapping {
return termshark.LoadKeyMappings()
}
func (g getMappings) None() bool {
return len(termshark.LoadKeyMappings()) == 0
}
//======================================================================
type MultiMenuOpener struct {
under gowid.IWidget
mm *MenuHolder
}
var _ menu.IOpener = (*MultiMenuOpener)(nil)
func (o *MultiMenuOpener) OpenMenu(mnu *menu.Widget, site menu.ISite, app gowid.IApp) bool {
if o.mm.IMenuCompatible != mnu {
// Adds the menu to the render tree - when not open, under is here instead
o.mm.IMenuCompatible = mnu
// Now make under the lower layer of the menu
mnu.SetSubWidget(o.under, app)
mnu.OpenImpl(site, app)
app.Redraw()
return true
} else {
return false
}
}
func (o *MultiMenuOpener) CloseMenu(mnu *menu.Widget, app gowid.IApp) {
if o.mm.IMenuCompatible == mnu {
mnu.CloseImpl(app)
o.mm.IMenuCompatible = holder.New(o.under)
}
}
//======================================================================
//
// Handle examples like
// .... ..1. .... .... .... .... = LG bit: Locally administered address (this is NOT the factory default)
// Extract just
// LG bit
//
// I'm trying to copy what Wireshark does, more or less
//
func columnNameFromShowname(showname string) string {
matches := shownameRe.FindStringSubmatch(showname)
if len(matches) >= 3 {
return matches[2]
}
return showname
}
func useAsColumn(filter string, name string, app gowid.IApp) {
newCols := profiles.ConfStringSlice("main.column-format", []string{})
colsBak := make([]string, len(newCols))
for i, col := range newCols {
colsBak[i] = col
}
newCols = append(newCols,
fmt.Sprintf("%%Cus:%s:0:R", filter),
columnNameFromShowname(name),
"true",
)
profiles.SetConf("main.column-format-bak", colsBak)
profiles.SetConf("main.column-format", newCols)
RequestReload(app)
}
// Build the menu dynamically when needed so I can include the filter in the widgets
func makePdmlFilterMenu(filter string, val string) *menu.Widget {
sites := make(menuutil.SiteMap)
needQuotes := false
ok, field := FieldCompleter.LookupField(filter)
// should be ok, because this filter comes from the PDML, so the filter should
// be valid. But if it isn't e.g. newer tshark perhaps, then assume no quotes
// are needed.
if ok {
switch field.Type {
case fields.FT_STRING:
needQuotes = true
case fields.FT_STRINGZ:
needQuotes = true
case fields.FT_STRINGZPAD:
needQuotes = true
}
}
filterStr := filter
if val != "" {
if needQuotes {
filterStr = fmt.Sprintf("%s == \"%s\"", filter, strings.ReplaceAll(val, "\\", "\\\\"))
} else {
filterStr = fmt.Sprintf("%s == %s", filter, val)
}
}
var pdmlFilterMenu *menu.Widget
openPdmlFilterMenu2 := func(prep bool, w gowid.IWidget, app gowid.IApp) {
st, ok := sites[w]
if !ok {
log.Warnf("Unexpected application state: missing menu site for %v", w)
return
}
// This contains logic to close the two PDML menus opened from the struct
// view and then to either apply or prepare a new display filter based on
// the one that is currently selected by the user (i.e. the one associated
// with the open menu)
actor := &pdmlFilterActor{
filter: filterStr,
prepare: prep,
menu1: pdmlFilterMenu,
}
menuBox := makeFilterCombineMenuWidget(actor)
m2 := menu.New("pdmlfilter2", menuBox, fixed, menu.Options{
Modal: true,
CloseKeysProvided: true,
CloseKeys: []gowid.IKey{
gowid.MakeKey('q'),
gowid.MakeKeyExt(tcell.KeyLeft),
gowid.MakeKeyExt(tcell.KeyEscape),
gowid.MakeKeyExt(tcell.KeyCtrlC),
},
})
// I need to set this up after constructing m2; m2 itself needs
// the menu box widget to display; that needs the actor to process
// the clicks of buttons within that widget, and that actor needs
// the menu m2 so that it can close it.
actor.menu2 = m2
multiMenu2Opener.OpenMenu(m2, st, app)
}
pdmlFilterItems := []menuutil.SimpleMenuItem{
menuutil.SimpleMenuItem{
Txt: fmt.Sprintf("Apply as Column: %s", filter),
Key: gowid.MakeKey('c'),
CB: func(app gowid.IApp, w gowid.IWidget) {
multiMenu1Opener.CloseMenu(pdmlFilterMenu, app)
useAsColumn(curColumnFilter, curColumnFilterName, app)
},
},
menuutil.MakeMenuDivider(),
menuutil.SimpleMenuItem{
Txt: fmt.Sprintf("Apply Filter: %s", filterStr),
Key: gowid.MakeKey('a'),
CB: func(app gowid.IApp, w gowid.IWidget) {
openPdmlFilterMenu2(false, w, app)
},
},
menuutil.SimpleMenuItem{
Txt: fmt.Sprintf("Prep Filter: %s", filterStr),
Key: gowid.MakeKey('p'),
CB: func(app gowid.IApp, w gowid.IWidget) {
openPdmlFilterMenu2(true, w, app)
},
},
}
pdmlFilterListBox, pdmlFilterWidth := menuutil.MakeMenuWithHotKeys(pdmlFilterItems, sites)
// this menu is opened from the PDML struct view and has, as context, the current PDML node. I
// need a name for it because I use that var in the closure above.
pdmlFilterMenu = menu.New("pdmlfiltermenu", pdmlFilterListBox, units(pdmlFilterWidth), menu.Options{
Modal: true,
CloseKeysProvided: true,
OpenCloser: &multiMenu1Opener,
CloseKeys: []gowid.IKey{
gowid.MakeKey('q'),
gowid.MakeKeyExt(tcell.KeyLeft),
gowid.MakeKeyExt(tcell.KeyEscape),
gowid.MakeKeyExt(tcell.KeyCtrlC),
},
})
return pdmlFilterMenu
}
//======================================================================
func RequestQuit() {
select {
case QuitRequestedChan <- struct{}{}:
default:
// Ok for the send not to succeed - there is a buffer of one, and it only
// needs one message to start the shutdown sequence. So this means a
// message has already been sent (before the main loop gets round to processing
// this channel)
}
}
// Runs in app goroutine
func UpdateProgressBarForInterface(c *pcap.InterfaceLoader, app gowid.IApp) {
SetProgressIndeterminateFor(app, LoaderOwns)
loadSpinner.Update()
}
// Runs in app goroutine
func UpdateProgressBarForFile(c *pcap.PacketLoader, prevRatio float64, app gowid.IApp) float64 {
SetProgressDeterminateFor(app, LoaderOwns)
psmlProg := Prog{0, 100}
pdmlPacketProg := Prog{0, 100}
pdmlIdxProg := Prog{0, 100}
pcapPacketProg := Prog{0, 100}
pcapIdxProg := Prog{0, 100}
curRowProg := Prog{100, 100}
var err error
var c2 int64
var m int64
var x int
// This shows where we are in the packet list. We want progress to be active only
// as long as our view has missing widgets. So this can help predict when our little
// view into the list of packets will be populated. Note that if a new pcap is loading,
// the packet list view should always be further away than the last packet, so we won't
// need the progress bar to tell the user how long until packets appear in the packet
// list view; but the packet struct and hex views are populated using a different
// mechanism (separate tshark processes) and may leave their views blank while the
// packet list view shows data - so the progress bar is useful to indicate when info
// will show up in the struct and hex views.
currentDisplayedRow := -1
var currentDisplayedRowMod int64 = -1
var currentDisplayedRowDiv int = -1
if packetListView != nil {
if fxy, err := packetListView.FocusXY(); err == nil {
currentRowId, ok := packetListView.Model().RowIdentifier(fxy.Row)
if ok {
pktsPerLoad := c.PacketsPerLoad()
currentDisplayedRow = int(currentRowId)
currentDisplayedRowMod = int64(currentDisplayedRow % pktsPerLoad)
// Rounded to 1000 by default
currentDisplayedRowDiv = (currentDisplayedRow / pktsPerLoad) * pktsPerLoad
c.PsmlLoader.Lock()
curRowProg.cur, curRowProg.max = int64(currentDisplayedRow), int64(len(c.PsmlData()))
c.PsmlLoader.Unlock()
}
}
}
// Progress determined by how many of the (up to) pktsPerLoad pdml packets are read
// If it's not the same chunk of rows, assume it won't affect our view, so no progress needed
if c.PdmlLoader.IsLoading() {
if c.LoadingRow() == currentDisplayedRowDiv {
// Data being loaded from pdml + pcap may overlap the current view
if x, err = c.LengthOfPdmlCacheEntry(c.LoadingRow()); err == nil {
pdmlPacketProg.cur = int64(x)
pdmlPacketProg.max = int64(c.KillAfterReadingThisMany)
if currentDisplayedRow != -1 && currentDisplayedRowMod < pdmlPacketProg.max {
pdmlPacketProg.max = currentDisplayedRowMod + 1 // zero-based
if pdmlPacketProg.cur > pdmlPacketProg.max {
pdmlPacketProg.cur = pdmlPacketProg.max
}
}
}
// Progress determined by how far through the pcap the pdml reader is.
c.PdmlLoader.Lock()
c2, m, err = system.ProcessProgress(c.PdmlPid, c.PcapPdml)
c.PdmlLoader.Unlock()
if err == nil {
pdmlIdxProg.cur, pdmlIdxProg.max = c2, m
if currentDisplayedRow != -1 {
// Only need to look this far into the psml file before my view is populated
m = m * (curRowProg.cur / curRowProg.max)
}
}
// Progress determined by how many of the (up to) pktsPerLoad pcap packets are read
if x, err = c.LengthOfPcapCacheEntry(c.LoadingRow()); err == nil {
pcapPacketProg.cur = int64(x)
pcapPacketProg.max = int64(c.KillAfterReadingThisMany)
if currentDisplayedRow != -1 && currentDisplayedRowMod < pcapPacketProg.max {
pcapPacketProg.max = currentDisplayedRowMod + 1 // zero-based
if pcapPacketProg.cur > pcapPacketProg.max {
pcapPacketProg.cur = pcapPacketProg.max
}
}
}
// Progress determined by how far through the pcap the pcap reader is.
c.PdmlLoader.Lock()
c2, m, err = system.ProcessProgress(c.PcapPid, c.PcapPcap)
c.PdmlLoader.Unlock()
if err == nil {
pcapIdxProg.cur, pcapIdxProg.max = c2, m
if currentDisplayedRow != -1 {
// Only need to look this far into the psml file before my view is populated
m = m * (curRowProg.cur / curRowProg.max)
}
}
}
}
if psml, ok := c.PcapPsml.(string); ok && c.PsmlLoader.IsLoading() {
c.PsmlLoader.Lock()
c2, m, err = system.ProcessProgress(termshark.SafePid(c.PsmlCmd), psml)
c.PsmlLoader.Unlock()
if err == nil {
psmlProg.cur, psmlProg.max = c2, m
}
}
var prog Prog
// state is guaranteed not to include pcap.Loadingiface if we showing a determinate progress bar
switch {
case c.PsmlLoader.IsLoading() && c.PdmlLoader.IsLoading() && c.PdmlLoader.LoadIsVisible():
select {
case <-c.StartStage2ChanFn():
prog = psmlProg.Add(
progMax(pcapPacketProg, pcapIdxProg).Add(
progMax(pdmlPacketProg, pdmlIdxProg),
),
)
default:
prog = psmlProg.Div(2) // temporarily divide in 2. Leave original for case above - so that the 50%
}
case c.PsmlLoader.IsLoading():
prog = psmlProg
case c.PdmlLoader.IsLoading() && c.PdmlLoader.LoadIsVisible():
prog = progMax(pcapPacketProg, pcapIdxProg).Add(
progMax(pdmlPacketProg, pdmlIdxProg),
)
}
curRatio := float64(prog.cur) / float64(prog.max)
if prevRatio < curRatio {
loadProgress.SetTarget(app, int(prog.max))
loadProgress.SetProgress(app, int(prog.cur))
}
return math.Max(prevRatio, curRatio)
}
//======================================================================
// psmlSummary is used to generate a summary for the marks dialog
type psmlSummary []string
func (p psmlSummary) String() string {
// Skip packet number
return strings.Join([]string(p)[1:], " : ")
}
//======================================================================
type RenderWeightUpTo struct {
gowid.RenderWithWeight
max int
}
func (s RenderWeightUpTo) MaxUnits() int {
return s.max
}
func weightupto(w int, max int) RenderWeightUpTo {
return RenderWeightUpTo{gowid.RenderWithWeight{W: w}, max}
}
func units(n int) gowid.RenderWithUnits {
return gowid.RenderWithUnits{U: n}
}
func weight(n int) gowid.RenderWithWeight {
return gowid.RenderWithWeight{W: n}
}
func ratio(r float64) gowid.RenderWithRatio {
return gowid.RenderWithRatio{R: r}
}
type RenderRatioUpTo struct {
gowid.RenderWithRatio
max int
}
func (r RenderRatioUpTo) String() string {
return fmt.Sprintf("upto(%v,%d)", r.RenderWithRatio, r.max)
}
func (r RenderRatioUpTo) MaxUnits() int {
return r.max
}
func ratioupto(f float64, max int) RenderRatioUpTo {
return RenderRatioUpTo{gowid.RenderWithRatio{R: f}, max}
}
//======================================================================
// run in app goroutine
func clearPacketViews(app gowid.IApp) {
packetHexWidgets.Purge()
packetListViewHolder.SetSubWidget(nullw, app)
packetStructureViewHolder.SetSubWidget(nullw, app)
packetHexViewHolder.SetSubWidget(nullw, app)
}
//======================================================================
// Construct decoration around the tree node widget - a button to collapse, etc.
func makeStructNodeDecoration(pos tree.IPos, tr tree.IModel, wmaker tree.IWidgetMaker) gowid.IWidget {
var res gowid.IWidget
if tr == nil {
return nil
}
// Note that level should never end up < 0
// We know our tree widget will never display the root node, so everything will be indented at
// least one level. So we know this will never end up negative.
level := -2
for cur := pos; cur != nil; cur = tree.ParentPosition(cur) {
level += 1
}
if level < 0 {
panic(errors.WithStack(gowid.WithKVs(termshark.BadState, map[string]interface{}{"level": level})))
}
pad := strings.Repeat(" ", level*2)
cwidgets := make([]gowid.IContainerWidget, 0)
cwidgets = append(cwidgets,
&gowid.ContainerWidget{
IWidget: text.New(pad),
D: units(len(pad)),
},
)
ct, ok := tr.(*pdmltree.Model)
if !ok {
panic(errors.WithStack(gowid.WithKVs(termshark.BadState, map[string]interface{}{"tree": tr})))
}
// Create an empty one here because the selectIf widget needs to have a pointer
// to it, and it's constructed below as a child.
rememberSel := &rememberSelected{}
inner := wmaker.MakeWidget(pos, tr)
inner = &selectIf{
IWidget: inner,
iWasSelected: rememberSel,
}
if ct.HasChildren() {
var bn *button.Widget
if ct.IsCollapsed() {
bn = button.NewAlt(text.New("+"))
} else {
bn = button.NewAlt(text.New("-"))
}
// If I use one button with conditional logic in the callback, rather than make
// a separate button depending on whether or not the tree is collapsed, it will
// correctly work when the DecoratorMaker is caching the widgets i.e. it will
// collapse or expand even when the widget is rendered from the cache
bn.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) {
// Run this outside current event loop because we are implicitly
// adjusting the data structure behind the list walker, and it's
// not prepared to handle that in the same pass of processing
// UserInput. TODO.
app.Run(gowid.RunFunction(func(app gowid.IApp) {
ct.SetCollapsed(app, !ct.IsCollapsed())
}))
}))
expandContractKeys := appkeys.New(
bn,
func(ev *tcell.EventKey, app gowid.IApp) bool {
handled := false
switch ev.Key() {
case tcell.KeyLeft:
if !ct.IsCollapsed() {
ct.SetCollapsed(app, true)
handled = true
}
case tcell.KeyRight:
if ct.IsCollapsed() {
ct.SetCollapsed(app, false)
handled = true
}
}
return handled
},
)
cwidgets = append(cwidgets,
&gowid.ContainerWidget{
IWidget: expandContractKeys,
D: fixed,
},
&gowid.ContainerWidget{
IWidget: fillSpace,
D: units(1),
},
)
} else {
// Lines without an expander are just text - so you can't cursor down on to them unless you
// make them selectable (because the list will jump over them)
inner = selectable.New(inner)
cwidgets = append(cwidgets,
&gowid.ContainerWidget{
IWidget: fillSpace,
D: units(4),
},
)
}
cwidgets = append(cwidgets, &gowid.ContainerWidget{
IWidget: inner,
D: weight(1),
})
res = columns.New(cwidgets)
rememberSel.IWidget = res
res = expander.New(
isselected.New(
rememberSel,
styled.New(rememberSel, gowid.MakePaletteRef("packet-struct-selected")),
styled.New(rememberSel, gowid.MakePaletteRef("packet-struct-focus")),
),
)
return res
}
// rememberSelected, when rendered, will save whether or not the selected flag was set.
// Another widget (deeper in the hierarchy) can then consult it to see whether it should
// render differently as the grandchild of a selected widget.
type rememberSelected struct {
gowid.IWidget
selectedThisTime bool
}
type iWasSelected interface {
WasSelected() bool
}
func (w *rememberSelected) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas {
w.selectedThisTime = focus.Selected
return w.IWidget.Render(size, focus, app)
}
func (w *rememberSelected) WasSelected() bool {
return w.selectedThisTime
}
// selectIf sets the selected flag on its child if its iWasSelected type returns true
type selectIf struct {
gowid.IWidget
iWasSelected
}
func (w *selectIf) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas {
if w.iWasSelected.WasSelected() {
focus = focus.SelectIf(true)
}
return w.IWidget.Render(size, focus, app)
}
// The widget representing the data at this level in the tree. Simply use what we extract from
// the PDML.
func makeStructNodeWidget(pos tree.IPos, tr tree.IModel) gowid.IWidget {
pdmlMenuButton := button.NewBare(text.New("[=]"))
pdmlMenuButtonSite := menu.NewSite(menu.SiteOptions{YOffset: 1})
pdmlMenuButton.OnClick(gowid.MakeWidgetCallback("cb", func(app gowid.IApp, w gowid.IWidget) {
curColumnFilter = tr.(*pdmltree.Model).Name
curColumnFilterValue = tr.(*pdmltree.Model).Show
curColumnFilterName = tr.(*pdmltree.Model).UiName
pdmlFilterMenu := makePdmlFilterMenu(curColumnFilter, curColumnFilterValue)
multiMenu1Opener.OpenMenu(pdmlFilterMenu, pdmlMenuButtonSite, app)
}))
styledButton1 := styled.New(pdmlMenuButton, gowid.MakePaletteRef("packet-struct-selected"))
styledButton2 := styled.New(
pdmlMenuButton,
gowid.MakeStyledAs(gowid.StyleBold),
)
structText := text.New(tr.Leaf())
structIfNotSel := columns.NewFixed(structText)
structIfSel := columns.NewFixed(structText, colSpace, pdmlMenuButtonSite, styledButton1)
structIfFocus := columns.NewFixed(structText, colSpace, pdmlMenuButtonSite, styledButton2)
return selectable.New(isselected.New(structIfNotSel, structIfSel, structIfFocus))
}
//======================================================================
// I want to have preferred position work on this, but you have to choose a subwidget
// to navigate to. We have three. I know that my use of them is very similar, so I'll
// just pick the first
type selectedComposite struct {
*isselected.Widget
}
var _ gowid.IComposite = (*selectedComposite)(nil)
func (w *selectedComposite) SubWidget() gowid.IWidget {
return w.Not
}
//======================================================================
// An ugly interface that captures what sort of type will be suitable
// as a table widget to which a row focus can be applied.
type iRowFocusTableWidgetNeeds interface {
gowid.IWidget
list.IBoundedWalker
table.IFocus
table.IGoToMiddle
table.ISetFocus
list.IWalkerHome
list.IWalkerEnd
SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp)
FocusXY() (table.Coords, error)
SetFocusXY(gowid.IApp, table.Coords)
SetModel(table.IModel, gowid.IApp)
Lower() *table.ListWithPreferedColumn
SetFocusOnData(app gowid.IApp) bool
OnFocusChanged(f gowid.IWidgetChangedCallback)
}
// rowFocusTableWidget provides a table that highlights the selected row or
// focused row.
type rowFocusTableWidget struct {
iRowFocusTableWidgetNeeds
rowSelected string
rowFocus string
}
func NewRowFocusTableWidget(w iRowFocusTableWidgetNeeds, rs string, rf string) *rowFocusTableWidget {
res := &rowFocusTableWidget{
iRowFocusTableWidgetNeeds: w,
rowSelected: rs,
rowFocus: rf,
}
res.Lower().IWidget = list.NewBounded(res)
return res
}
var _ gowid.IWidget = (*rowFocusTableWidget)(nil)
func (t *rowFocusTableWidget) SubWidget() gowid.IWidget {
return t.iRowFocusTableWidgetNeeds
}
func (t *rowFocusTableWidget) InvertedModel() table.IInvertible {
return t.Model().(table.IInvertible)
}
func (t *rowFocusTableWidget) Rows() int {
return t.Model().(table.IBoundedModel).Rows()
}
// Implement withscrollbar.IScrollValues
func (t *rowFocusTableWidget) ScrollLength() int {
return t.Rows()
}
// Implement withscrollbar.IScrollValues
func (t *rowFocusTableWidget) ScrollPosition() int {
return t.CurrentRow()
}
func (t *rowFocusTableWidget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) {
for i := 0; i < lines; i++ {
t.UserInput(tcell.NewEventKey(tcell.KeyUp, ' ', tcell.ModNone), size, gowid.Focused, app)
}
}
func (t *rowFocusTableWidget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) {
for i := 0; i < lines; i++ {
t.UserInput(tcell.NewEventKey(tcell.KeyDown, ' ', tcell.ModNone), size, gowid.Focused, app)
}
}
func (t *rowFocusTableWidget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) {
for i := 0; i < num; i++ {
t.UserInput(tcell.NewEventKey(tcell.KeyPgUp, ' ', tcell.ModNone), size, gowid.Focused, app)
}
}
func (t *rowFocusTableWidget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) {
for i := 0; i < num; i++ {
t.UserInput(tcell.NewEventKey(tcell.KeyPgDn, ' ', tcell.ModNone), size, gowid.Focused, app)
}
}
// list.IWalker
func (t *rowFocusTableWidget) At(lpos list.IWalkerPosition) gowid.IWidget {
pos := int(lpos.(table.Position))
w := t.AtRow(pos)
if w == nil {
return nil
}