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/dbg"
"github.com/pancsta/asyncmachine-go/tools/debugger/types"
)
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 (tcell .GetColor (theme .Highlight2 ))
tree .SetSelectedTextColor (tcell .GetColor (theme .White ))
tree .SetHighlightColor (tcell .GetColor (theme .Highlight ))
tree .SetScrollBarColor (tcell .GetColor (theme .Highlight2 ))
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 , Pass (&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 dbg .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 nextTx != nil && c .CursorTx1 < len (c .MsgTxs ) && c .CursorStep1 > 0 {
steps = nextTx .Steps
}
colIdx := d .hUpdateTreeDefaultsHighlights (msg , i1 )
if nextTx != nil {
colIdx = max (colIdx , d .hUpdateTreeTxSteps (steps , nextTx ))
colIdx += treeIndent
}
d .hSortTree ()
d .hUpdateTreeRelCols (colIdx , steps , nextTx )
}
func (d *Debugger ) hUpdateTreeDefaultsHighlights (
msg dbg .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 (" " ,
max (0 , maxNameLen -len (stateName )))
nodeColor := theme .Inactive
if msg .Is (index , am .S {stateName }) {
if stateName == am .StateException ||
strings .HasPrefix (stateName , am .PrefixErr ) {
nodeColor = theme .Err
} else {
nodeColor = theme .Active
}
}
node .SetText (stateNamePad )
multi := " "
if s , ok := schema [stateName ]; ok && !ref .isRef && s .Multi {
multi = "M"
if nodeColor == theme .Active {
nodeColor = theme .Active2
}
}
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 (tcell .GetColor (nodeColor ))
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 (tcell .GetColor (nodeColor ))
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 *dbg .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 ) +
"[" + theme .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 dbg .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 += "[" + theme .Grey + "]."
} else if !thirdSpace {
thirdSpace = true
dotted += "[" + theme .Grey + "]."
} else {
dotted += "."
}
} else if secondSpace && !white {
white = true
dotted += "[" + theme .White + "]" + string (t )
} else {
dotted += string (t )
}
}
node .SetText (dotted + "[" + theme .Grey + "]" )
nodeCols = strings .Repeat ("." , max (0 , spaces ))
} else {
nodeCols = strings .Repeat (" " , max (0 , spaces ))
}
if len (relCols ) > 0 {
nodeCols += "[" + theme .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 += "[" + theme .Green + "::b]|[" + theme .Grey + "::-]"
} else if isRelEnd {
nodeCols += "[" + theme .Err + "::b]|[" + theme .Grey + "::-]"
} else {
nodeCols += "|"
}
active ++
} else if isAnyStart || isAnyEnd {
nodeCols += "."
} else {
nodeCols += " "
}
}
suffix := trailingDots .ReplaceAllString (nodeCols , "" )
text := node .GetText ()
if ref .stateName != "" && !ref .isRef && msg != nil {
if !msg .Is (d .C .MsgStruct .StatesIndex , am .S {ref .stateName }) {
text = reTreeStateColorFix .ReplaceAllString (text ,
"[" +theme .Inactive +"]$1[" +theme .Grey +"]$2" )
} else {
text = reTreeStateColorFix .ReplaceAllString (text ,
"[" +theme .Active +"]$1[" +theme .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 (tcell .GetColor (theme .Inactive ))
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 .GetColor (theme .Grey ))
tagNode .SetReference (&nodeRef {
isTag : true ,
})
tagRootNode .AddChild (tagNode )
}
stateNode .AddChild (tagRootNode )
}
stateNode .SetExpanded (false )
}
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 types .NormalizeGroupName (name ) ==
types .NormalizeGroupName (d .C .SelectedGroup ) {
sel = i
}
}
d .treeGroups .ClearOptions ()
d .treeGroups .AddOptions (opts ...)
d .treeGroupSkip = true
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.4 . (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 .