package debugger
import (
"fmt"
"regexp"
"slices"
"strconv"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/pancsta/cview"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
"github.com/pancsta/asyncmachine-go/pkg/telemetry"
ss "github.com/pancsta/asyncmachine-go/tools/debugger/states"
)
type nodeRef struct {
stateName string
isRef bool
isRel bool
rel am .Relation
parentState string
touched bool
expanded bool
isProp bool
propLabel string
isTagRoot bool
isTag bool
}
const treeIndent = 3
var trailingDots = regexp .MustCompile (`\.+$` )
func (d *Debugger ) hInitSchemaTree () *cview .TreeView {
d .treeRoot = cview .NewTreeNode ("States" )
tree := cview .NewTreeView ()
tree .SetRoot (d .treeRoot )
tree .SetCurrentNode (d .treeRoot )
tree .SetSelectedBackgroundColor (colorHighlight2 )
tree .SetSelectedTextColor (tcell .ColorWhite )
tree .SetHighlightColor (colorHighlight )
tree .SetScrollBarColor (colorHighlight2 )
tree .SetChangedFunc (func (node *cview .TreeNode ) {
ref , ok := node .GetReference ().(*nodeRef )
if !ok || ref .stateName == "" {
d .Mach .Remove1 (ss .StateNameSelected , nil )
d .lastSelectedState = ""
return
}
d .Mach .Add1 (ss .StateNameSelected , am .A {
"state" : ref .stateName ,
})
d .hUpdateLogReader (nil )
d .hUpdateMatrix ()
})
tree .SetSelectedFunc (func (node *cview .TreeNode ) {
ref , ok := node .GetReference ().(*nodeRef )
if !ok {
return
}
if ref .isRef && ref .stateName != "" {
name := normalizeText (ref .stateName )
for _ , child := range d .treeRoot .GetChildren () {
if name == normalizeText (strings .Split (child .GetText (), " " )[0 ]) {
d .tree .SetCurrentNode (child )
node .SetHighlighted (true )
return
}
}
}
curr := d .tree .GetCurrentNode ()
if curr == node {
ref .expanded = !node .IsExpanded ()
node .SetExpanded (ref .expanded )
}
})
return tree
}
func (d *Debugger ) hUpdateSchemaTree () {
var msg telemetry .DbgMsg
c := d .C
if c == nil {
return
}
i1 := 0
if c .CursorTx1 == 0 {
msg = c .MsgStruct
} else {
i1 = c .CursorTx1 - 1
msg = c .MsgTxs [i1 ]
}
d .tree .SetTitle (d .P .Sprintf (" Schema:%v " , len (c .MsgStruct .StatesIndex )))
var steps []*am .Step
nextTx := d .hNextTx ()
if c .CursorTx1 < len (c .MsgTxs ) && c .CursorStep1 > 0 {
steps = nextTx .Steps
}
colIdx := d .hUpdateTreeDefaultsHighlights (msg , i1 )
colIdx = max (colIdx , d .hUpdateTreeTxSteps (steps , nextTx ))
colIdx += treeIndent
d .hSortTree ()
d .hUpdateTreeRelCols (colIdx , steps , nextTx )
}
func (d *Debugger ) hUpdateTreeDefaultsHighlights (
msg telemetry .DbgMsg , idx int ,
) int {
c := d .C
if c == nil {
return 0
}
maxNameLen := 0
index := c .MsgStruct .StatesIndex
for _ , name := range index {
maxNameLen = max (maxNameLen , len (name ))
}
schema := c .MsgStruct .States
maxLen := 0
d .tree .GetRoot ().Walk (func (
node , parent *cview .TreeNode , depth int ,
) bool {
if parent == nil {
return true
}
ref , ok := node .GetReference ().(*nodeRef )
if !ok {
maxLen = maxNodeLen (node , maxLen , depth )
return true
}
ref .touched = false
node .SetUnderline (false )
if ref .isRel {
node .SetText (capitalizeFirst (ref .rel .String ()))
return true
} else if ref .isProp {
node .SetText (ref .propLabel )
maxLen = maxNodeLen (node , maxLen , depth )
return true
} else if ref .isTag {
return true
} else if ref .isTagRoot {
node .SetText ("Tags" )
maxLen = maxNodeLen (node , maxLen , depth )
return true
}
if parent == d .tree .GetRoot () || !parent .GetHighlighted () {
node .SetHighlighted (false )
}
stateName := ref .stateName
stateNamePad := stateName + strings .Repeat (" " , maxNameLen -len (stateName ))
color := colorInactive
if msg .Is (index , am .S {stateName }) {
if stateName == am .StateException ||
strings .HasPrefix (stateName , am .PrefixErr ) {
color = colorErr
} else {
color = colorActive
}
}
node .SetText (stateNamePad )
multi := " "
if s , ok := schema [stateName ]; ok && !ref .isRef && s .Multi {
multi = "M"
if color == colorActive {
color = colorActive2
}
}
if stateName != c .SelectedState {
if !ref .isRef {
for _ , child := range node .GetChildren () {
child .SetHighlighted (false )
for _ , child2 := range child .GetChildren () {
child2 .SetHighlighted (false )
}
}
tick := d .P .Sprintf ("%d" , msg .Clock (index ,
stateName ))
node .SetColor (color )
node .SetText (stateNamePad + " " + multi + "|" + tick )
}
maxLen = maxNodeLen (node , maxLen , depth )
return true
}
if node != d .tree .GetCurrentNode () {
node .SetHighlighted (true )
}
if ref .isRef {
maxLen = maxNodeLen (node , maxLen , depth )
return true
}
tick := strconv .FormatUint (msg .Clock (index ,
stateName ), 10 )
node .SetColor (color )
node .SetText (stateNamePad + " " + multi + "|" + tick )
maxLen = maxNodeLen (node , maxLen , depth )
if node == d .tree .GetCurrentNode () {
return true
}
for _ , child := range node .GetChildren () {
child .SetHighlighted (true )
for _ , child2 := range child .GetChildren () {
child2 .SetHighlighted (true )
}
}
return true
})
return maxLen
}
func (d *Debugger ) hUpdateTreeTxSteps (
steps []*am .Step , tx *telemetry .DbgMsgTx ,
) int {
c := d .C
if c == nil {
return 0
}
maxLen := 0
if c .CursorStep1 < 1 {
for _ , node := range d .tree .GetRoot ().GetChildren () {
ref , ok := node .GetReference ().(*nodeRef )
if !ok {
continue
}
d .handleExpanded (node , ref , c )
}
return 0
}
d .tree .GetRoot ().Walk (func (
node , parent *cview .TreeNode , depth int ,
) bool {
if parent != nil {
maxLen = maxNodeLen (node , maxLen , depth )
}
return true
})
maxLenTagged := maxLen
d .tree .GetRoot ().Walk (func (
node , parent *cview .TreeNode , depth int ,
) bool {
if parent == nil {
return true
}
ref , ok := node .GetReference ().(*nodeRef )
if !ok {
return true
}
states := c .MsgStruct .StatesIndex
if ref .stateName != "" {
stateName := ref .stateName
for i := range steps {
if c .CursorStep1 == i {
break
}
step := steps [i ]
textMargin := ""
visibleLen := node .VisibleLength ()
if maxLen +1 -visibleLen > 0 {
textMargin = strings .Repeat (" " , maxLen +1 -visibleLen ) + "[white]"
}
switch step .Type {
case am .StepRemoveNotActive :
if step .GetToState (states ) == stateName && !ref .isRef {
nodeSetBold (node )
ref .touched = true
}
case am .StepRemove :
if step .GetToState (states ) == stateName && !ref .isRef {
node .SetText (node .GetText () + textMargin + "[::b]-[::-]" )
nodeSetBold (node )
ref .touched = true
}
case am .StepRelation :
if step .GetFromState (states ) == stateName && !ref .isRef {
nodeSetBold (node )
ref .touched = true
} else if step .GetToState (states ) == stateName && !ref .isRef {
nodeSetBold (node )
ref .touched = true
} else if ref .isRef && step .GetToState (states ) == stateName &&
ref .parentState == step .GetFromState (states ) {
nodeSetBold (node )
ref .touched = true
}
case am .StepHandler :
if ref .isRef {
continue
}
if step .GetFromState (states ) == stateName ||
step .GetToState (states ) == stateName {
if !tx .Accepted && i == len (steps )-1 {
node .SetText (node .GetText () + textMargin + "[red::b]*[-::-]" )
} else {
node .SetText (node .GetText () + textMargin + "[::b]*[::-]" )
}
nodeSetBold (node )
ref .touched = true
}
case am .StepSet :
if step .GetToState (states ) == stateName && !ref .isRef {
node .SetText (node .GetText () + textMargin + "[::b]+[::-]" )
nodeSetBold (node )
ref .touched = true
}
case am .StepRequested :
if step .GetToState (states ) == stateName && !ref .isRef {
text := node .GetText ()
idx := strings .Index (text , " " )
node .SetText ("[::bu]" + text [:idx ] + "[::-]" + text [idx :])
ref .touched = true
}
case am .StepCancel :
if step .GetToState (states ) == stateName && !ref .isRef {
if !tx .Accepted && i == len (steps )-1 {
node .SetText (node .GetText () + textMargin + "[red::b]![-::-]" )
} else {
node .SetText (node .GetText () + textMargin + "[::b]![::-]" )
}
nodeSetBold (node )
ref .touched = true
}
}
}
d .handleExpanded (node , ref , c )
} else if ref .isRel {
for i := range steps {
if c .CursorStep1 == i {
break
}
step := steps [i ]
if step .Type != am .StepRelation {
continue
}
if step .RelType == ref .rel &&
ref .parentState == step .GetFromState (states ) {
nodeSetBold (node )
ref .touched = true
}
}
} else if ref .isTagRoot {
node .Collapse ()
}
maxLenTagged = maxNodeLen (node , maxLenTagged , depth )
return true
})
return maxLenTagged
}
var reTreeStateColorFix = regexp .MustCompile (`\[white\](M?\|\d+)(\.*)` )
func (d *Debugger ) hUpdateTreeRelCols (
colStartIdx int , steps []*am .Step , msg telemetry .DbgMsg ,
) {
c := d .C
if c == nil {
return
}
if c .CursorStep1 < 1 {
return
}
var relCols []RelCol
var closed bool
d .tree .GetRoot ().Walk (func (
node , parent *cview .TreeNode , depth int ,
) bool {
if parent == nil || !parentExpanded (node ) {
return true
}
ref , ok := node .GetReference ().(*nodeRef )
if !ok {
return true
}
var forcedCols []string
if ref .stateName != "" {
stateName := ref .stateName
for i := range steps {
if c .CursorStep1 == i {
break
}
step := steps [i ]
if step .Type != am .StepRelation {
continue
}
index := c .MsgStruct .StatesIndex
isTarget := step .GetToState (index ) == stateName && !ref .isRef
isSource := ref .isRef && step .GetToState (index ) == stateName &&
ref .parentState == step .GetFromState (index )
if isTarget || isSource {
colName := getRelColName (index , step )
relCols , closed = handleTreeCol (strconv .Itoa (depth ), colName , relCols )
if closed {
forcedCols = append (forcedCols , colName )
}
}
}
}
isAnyStart := false
isAnyEnd := false
for _ , col := range relCols {
isAnyStart = ref .isRef && ref .stateName != "" &&
col .name == getRelColNameFromRef (ref )
if isAnyStart {
break
}
isAnyEnd = !ref .isRef && ref .stateName != "" &&
strings .HasSuffix (col .name , ref .stateName )
if isAnyEnd {
break
}
}
nodeColStartIdx := colStartIdx - depth *treeIndent + treeIndent
nodeCols := ""
spaces := nodeColStartIdx - node .VisibleLength ()
if isAnyStart || isAnyEnd {
dotted := ""
firstSpace := false
secondSpace := false
thirdSpace := false
white := false
for _ , t := range node .GetText () {
if t == ' ' && !firstSpace {
firstSpace = true
dotted += " "
continue
}
if t == ' ' {
if !secondSpace {
secondSpace = true
dotted += "[grey]."
} else if !thirdSpace {
thirdSpace = true
dotted += "[grey]."
} else {
dotted += "."
}
} else if secondSpace && !white {
white = true
dotted += "[white]" + string (t )
} else {
dotted += string (t )
}
}
node .SetText (dotted + "[grey]" )
nodeCols = strings .Repeat ("." , max (0 , spaces ))
} else {
nodeCols = strings .Repeat (" " , max (0 , spaces ))
}
if len (relCols ) > 0 {
nodeCols += "[grey]"
}
active := 0
for _ , col := range relCols {
forced := false
for _ , forcedCol := range forcedCols {
if forcedCol == col .name {
forced = true
}
}
isRelStart := ref .isRef && ref .stateName != "" &&
col .name == getRelColNameFromRef (ref )
isRelEnd := !ref .isRef && ref .stateName != "" &&
strings .HasSuffix (col .name , "-" +ref .stateName )
if !col .closed || forced {
if isRelStart {
nodeCols += "[green::b]|[grey::-]"
} else if isRelEnd {
nodeCols += "[red::b]|[grey::-]"
} else {
nodeCols += "|"
}
active ++
} else if isAnyStart || isAnyEnd {
nodeCols += "."
} else {
nodeCols += " "
}
}
suffix := trailingDots .ReplaceAllString (nodeCols , "" )
text := node .GetText ()
if ref .stateName != "" && !ref .isRef {
if !msg .Is (d .C .MsgStruct .StatesIndex , am .S {ref .stateName }) {
text = reTreeStateColorFix .ReplaceAllString (text ,
"[" +colorInactive .String ()+"]$1[grey]$2" )
} else {
text = reTreeStateColorFix .ReplaceAllString (text ,
"[" +colorActive .String ()+"]$1[grey]$2" )
}
}
node .SetText (text + suffix )
return true
})
}
func (d *Debugger ) handleExpanded (
node *cview .TreeNode , ref *nodeRef , c *Client ,
) {
if ref .isRef || ref .stateName == "" {
return
}
stepsMode := c .CursorStep1 > 0 || d .Mach .Is1 (ss .TimelineStepsFocused )
node .SetExpanded (false )
if (ref .expanded && !stepsMode ) || (ref .touched && stepsMode ) {
node .SetExpanded (true )
}
}
func (d *Debugger ) hBuildSchemaTree () {
c := d .C
msg := c .MsgStruct
d .treeRoot .ClearChildren ()
states := msg .StatesIndex
if c .SelectedGroup != "" {
states = c .MsgSchemaParsed .Groups [c .SelectedGroup ]
}
d .schemaTreeStates = states
for _ , name := range states {
d .hAddState (name )
}
d .treeRoot .CollapseAll ()
d .treeRoot .Expand ()
}
func (d *Debugger ) hSelectTreeState (name string ) {
if d .tree == nil {
return
}
d .tree .GetRoot ().Walk (func (node , p *cview .TreeNode , depth int ) bool {
if p == nil {
return true
}
ref := node .GetReference ().(*nodeRef )
if ref .stateName == name && depth == 1 {
d .tree .SetCurrentNode (node )
return false
}
return true
})
}
func (d *Debugger ) hAddState (name string ) {
c := d .C
if c == nil {
return
}
state := c .MsgStruct .States [name ]
labels := ""
if state .Auto {
labels += "auto"
}
multi := " "
if state .Multi {
if labels != "" {
labels += " "
}
labels += "multi"
multi = "M"
}
stateNode := cview .NewTreeNode (name + " " + multi + "|0" )
stateNode .SetSelectable (true )
stateNode .SetReference (&nodeRef {stateName : name })
stateNode .SetColor (colorInactive )
d .treeRoot .AddChild (stateNode )
if labels != "" {
labelNode := cview .NewTreeNode (labels )
labelNode .SetReference (&nodeRef {
isProp : true ,
propLabel : labels ,
})
stateNode .AddChild (labelNode )
}
addRelation (stateNode , name , am .RelationAdd , state .Add , d .schemaTreeStates )
addRelation (stateNode , name , am .RelationRequire , state .Require ,
d .schemaTreeStates )
addRelation (stateNode , name , am .RelationRemove , state .Remove ,
d .schemaTreeStates )
addRelation (stateNode , name , am .RelationAfter , state .After ,
d .schemaTreeStates )
if len (state .Tags ) > 0 {
tagRootNode := cview .NewTreeNode ("Tags" )
tagRootNode .SetSelectable (true )
tagRootNode .SetReference (&nodeRef {
isTagRoot : true ,
})
for _ , tag := range state .Tags {
tagNode := cview .NewTreeNode ("#" + tag )
tagNode .SetColor (tcell .ColorGrey )
tagNode .SetReference (&nodeRef {
isTag : true ,
})
tagRootNode .AddChild (tagNode )
}
stateNode .AddChild (tagRootNode )
}
}
func (d *Debugger ) hSortTree () {
nodes := d .treeRoot .GetChildren ()
slices .SortStableFunc (nodes , func (a , b *cview .TreeNode ) int {
refA := a .GetReference ().(*nodeRef )
refB := b .GetReference ().(*nodeRef )
if refA .touched && !refB .touched {
return -1
} else if !refA .touched && refB .touched {
return 1
}
idxA := slices .Index (d .C .MsgStruct .StatesIndex , refA .stateName )
idxB := slices .Index (d .C .MsgStruct .StatesIndex , refB .stateName )
if idxA < idxB {
return -1
} else {
return 1
}
})
d .treeRoot .SetChildren (nodes )
}
func (d *Debugger ) hUpdateTreeGroups () {
var sel int
var opts []*cview .DropDownOption
for i , name := range d .C .MsgSchemaParsed .GroupsOrder {
amount := len (d .C .MsgSchemaParsed .Groups [name ])
label := "all"
if name != "all" {
label = fmt .Sprintf ("%s:%d" , name , amount )
}
opts = append (opts , cview .NewDropDownOption (label ))
if name == d .C .SelectedGroup {
sel = i
}
}
d .treeGroups .ClearOptions ()
d .treeGroups .AddOptions (opts ...)
go d .treeGroups .SetCurrentOption (sel )
}
func parentExpanded(node *cview .TreeNode ) bool {
for node = node .GetParent (); node != nil ; node = node .GetParent () {
if !node .IsExpanded () {
return false
}
}
return true
}
func handleTreeCol(source , name string , relCols []RelCol ) ([]RelCol , bool ) {
closed := false
for i , col := range relCols {
if col .name == name && col .source != source {
relCols [i ].closed = true
closed = true
}
}
if closed {
return relCols , true
}
relCols = append (relCols , RelCol {
colIndex : len (relCols ),
name : name ,
source : source ,
})
return relCols , false
}
func getRelColName(stateNames am .S , step *am .Step ) string {
return step .GetFromState (stateNames ) + "-" +
step .RelType .String () + "-" + step .GetToState (stateNames )
}
func getRelColNameFromRef(ref *nodeRef ) string {
return ref .parentState + "-" + ref .rel .String () + "-" + ref .stateName
}
func addRelation(
stateNode *cview .TreeNode , parentState string , rel am .Relation ,
relations []string , statesWhitelist am .S ,
) {
if len (relations ) <= 0 {
return
}
relNode := cview .NewTreeNode (capitalizeFirst (rel .String ()))
relNode .SetSelectable (true )
relNode .SetReference (&nodeRef {
isRel : true ,
rel : rel ,
parentState : parentState ,
})
for i := range relations {
relState := relations [i ]
stateNode := cview .NewTreeNode (relState )
stateNode .SetReference (&nodeRef {
isRef : true ,
rel : rel ,
stateName : relState ,
parentState : parentState ,
})
relNode .AddChild (stateNode )
}
stateNode .AddChild (relNode )
}
type RelCol struct {
name string
closed bool
colIndex int
source string
}
func capitalizeFirst(s string ) string {
if len (s ) == 0 {
return s
}
return strings .ToUpper (string (s [0 ])) + s [1 :]
}
func maxNodeLen(node *cview .TreeNode , maxLen int , depth int ) int {
return max (maxLen , node .VisibleLength ()+(depth -1 )*3 )
}
func nodeSetBold(node *cview .TreeNode ) {
txt := node .GetText ()
if strings .Contains (txt , "[::b]" ) {
return
}
idx := strings .Index (txt , " " )
if idx < 0 {
node .SetText ("[::b]" + txt + "[::-]" )
return
}
node .SetText ("[::b]" + txt [:idx ] + "[::-]" + txt [idx :])
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .