package debugger
import (
"bufio"
"context"
_ "embed"
"encoding/gob"
"fmt"
"log"
"net/http"
"net/url"
"os"
"path"
"runtime"
"slices"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/PuerkitoBio/goquery"
"github.com/andybalholm/brotli"
"github.com/gdamore/tcell/v2"
"github.com/pancsta/cview"
"github.com/zyedidia/clipper"
"golang.org/x/text/message"
"github.com/pancsta/asyncmachine-go/tools/debugger/server"
"github.com/pancsta/asyncmachine-go/tools/debugger/types"
"github.com/pancsta/asyncmachine-go/internal/utils"
amgraph "github.com/pancsta/asyncmachine-go/pkg/graph"
amhelp "github.com/pancsta/asyncmachine-go/pkg/helpers"
am "github.com/pancsta/asyncmachine-go/pkg/machine"
ssam "github.com/pancsta/asyncmachine-go/pkg/states"
"github.com/pancsta/asyncmachine-go/pkg/telemetry"
ss "github.com/pancsta/asyncmachine-go/tools/debugger/states"
amvis "github.com/pancsta/asyncmachine-go/tools/visualizer"
)
type S = am .S
type cache struct {
diagramId string
diagramLvl int
diagramDom *goquery .Document
}
type Debugger struct {
*am .ExceptionHandler
Mach *am .Machine
Clients map [string ]*Client
Opts Opts
LayoutRoot *cview .Panels
C *Client
App *cview .Application
P *message .Printer
History []*types .MachAddress
HistoryCursor int
drawing atomic .Bool
cache cache
tree *cview .TreeView
treeRoot *cview .TreeNode
log *cview .TextView
timelineTxs *cview .ProgressBar
timelineSteps *cview .ProgressBar
focusable []*cview .Box
playTimer *time .Ticker
currTxBarRight *cview .TextView
currTxBarLeft *cview .TextView
nextTxBarLeft *cview .TextView
nextTxBarRight *cview .TextView
helpDialog *cview .Flex
statusBar *cview .TextView
clientList *cview .List
mainGrid *cview .Grid
logRebuildEnd int
lastScrolledTxTime time .Time
repaintScheduled atomic .Bool
repaintPending atomic .Bool
genGraphsLast time .Time
graph *amgraph .Graph
updateCLScheduled atomic .Bool
buildCLScheduled atomic .Bool
lastKeystroke tcell .Key
lastKeystrokeTime time .Time
matrix *cview .Table
focusManager *cview .FocusManager
exportDialog *cview .Modal
contentPanels *cview .Panels
toolbars [3 ]*cview .Table
schemaLogGrid *cview .Grid
treeMatrixGrid *cview .Grid
lastSelectedState string
redrawCallback func ()
heartbeatT *time .Ticker
logReader *cview .TreeView
helpDialogRight *cview .TextView
addressBar *cview .Table
tagsBar *cview .TextView
clip clipper .Clipboard
toolbarItems [][]toolbarItem
clientListFile *os .File
txListFile *os .File
msgsDelayed []*telemetry .DbgMsgTx
msgsDelayedConns []string
currTxBar *cview .Flex
nextTxBar *cview .Flex
mainGridCols []int
readerExpanded map [string ]bool
treeGroups *cview .DropDown
treeLayout *cview .Flex
schemaTreeStates am .S
lastSelectedGroup string
logAppends int
logRenderedClient string
lastResize uint64
logLastResize uint64
}
func New (ctx context .Context , opts Opts ) (*Debugger , error ) {
var err error
d := &Debugger {
Clients : make (map [string ]*Client ),
readerExpanded : make (map [string ]bool ),
}
d .Opts = opts
if d .Opts .Filters == nil {
d .Opts .Filters = &OptsFilters {
LogLevel : am .LogChanges ,
}
}
if d .Opts .MaxMemMb == 0 {
d .Opts .MaxMemMb = maxMemMb
}
if d .Opts .Log2Ttl == 0 {
d .Opts .Log2Ttl = time .Hour
}
gob .Register (server .Exportable {})
gob .Register (am .Relation (0 ))
id := utils .RandId (0 )
if opts .Id != "" {
id = opts .Id
}
mach , err := am .NewCommon (ctx , "d-" +id , ss .States , ss .Names , d , nil , &am .Opts {
DontLogId : true ,
Tags : []string {"am-dbg" },
})
if err != nil {
return nil , err
}
d .Mach = mach
mach .SetGroupsString (map [string ]am .S {
"Dialog" : ss .GroupDialog ,
"Playing" : ss .GroupPlaying ,
"Focused" : ss .GroupFocused ,
"Views" : ss .GroupViews ,
"SwitchedClientTx" : ss .GroupSwitchedClientTx ,
"Filters" : ss .GroupFilters ,
"Debug" : ss .GroupDebug ,
}, []string {
"Dialog" ,
"Playing" ,
"Focused" ,
"Views" ,
"SwitchedClientTx" ,
"Filters" ,
"Debug" ,
})
if d .Opts .Version == "" {
d .Opts .Version = "(devel)"
}
semLog := mach .SemLogger ()
if opts .DbgLogger != nil {
semLog .SetSimple (opts .DbgLogger .Printf , opts .DbgLogLevel )
} else {
semLog .SetSimple (log .Printf , opts .DbgLogLevel )
}
semLog .SetArgsMapper (am .NewArgsMapper ([]string {
"Client.id" , "conn_id" , "cursorTx1" , "amount" , "Client.id" ,
"state" , "fwd" , "filter" , "ToolName" , "uri" , "addr" , "url" ,
"err" , "_am_err" ,
}, 20 ))
d .graph , err = amgraph .New (d .Mach )
if err != nil {
mach .AddErr (fmt .Errorf ("graph init: %w" , err ), nil )
}
if opts .ImportData != "" {
fmt .Printf ("Importing data from %s\nPlease wait...\n" , opts .ImportData )
start := time .Now ()
mach .Log ("Importing data from %s" , opts .ImportData )
d .hImportData (opts .ImportData )
mach .Log ("Imported data in %s" , time .Since (start ))
}
if d .Opts .EnableClipboard {
clip , err := clipper .GetClipboard (clipper .Clipboards ...)
if err != nil {
mach .AddErr (fmt .Errorf ("clipboard init: %w" , err ), nil )
}
d .clip = clip
}
if d .Opts .OutputDir != "" && d .Opts .OutputDir != "." {
if err := os .MkdirAll (d .Opts .OutputDir , 0o755 ); err != nil {
mach .AddErr (fmt .Errorf ("create output dir: %w" , err ), nil )
}
}
if d .Opts .OutputClients {
p := path .Join (d .Opts .OutputDir , "am-dbg-clients.txt" )
clientListFile , err := os .Create (p )
if err != nil {
mach .AddErr (fmt .Errorf ("client list file open: %w" , err ), nil )
}
d .clientListFile = clientListFile
}
if d .Opts .OutputTx {
p := path .Join (d .Opts .OutputDir , "am-dbg-tx.md" )
txListFile , err := os .Create (p )
if err != nil {
mach .AddErr (fmt .Errorf ("tx list file open: %w" , err ), nil )
}
d .txListFile = txListFile
}
return d , nil
}
func (d *Debugger ) hGetMachAddress () *types .MachAddress {
c := d .C
if c == nil {
return nil
}
a := &types .MachAddress {
MachId : c .Id ,
MachTime : c .MTimeSum ,
}
if c .CursorTx1 > 0 {
a .TxId = c .MsgTxs [c .CursorTx1 -1 ].ID
}
if c .CursorStep1 > 0 {
a .Step = c .CursorStep1
}
return a
}
func (d *Debugger ) hGoToMachAddress (
addr *types .MachAddress , skipHistory bool ,
) bool {
if addr .MachId == "" {
return false
}
var wait <-chan struct {}
mach := d .Mach
if d .C == nil || d .C .Id != addr .MachId {
if mach .Is1 (ss .ClientSelected ) {
wait = mach .WhenTicks (ss .ClientSelected , 2 , nil )
} else {
wait = mach .When1 (ss .ClientSelected , nil )
}
res := mach .Add1 (ss .SelectingClient , am .A {"Client.id" : addr .MachId })
if res == am .Canceled {
return false
}
} else {
wait = mach .When1 (ss .ClientSelected , nil )
}
<-wait
if addr .TxId != "" {
args := am .A {"Client.txId" : addr .TxId }
if addr .Step != 0 {
args ["cursorStep1" ] = addr .Step
mach .Add1 (ss .ScrollToStep , args )
}
mach .Add1 (ss .ScrollToTx , args )
} else if addr .MachTime != 0 {
tx := d .C .Tx (d .C .TxByMachTime (addr .MachTime ))
mach .Add1 (ss .ScrollToTx , am .A {"Client.txId" : tx .ID })
} else if !addr .HumanTime .IsZero () {
tx := d .C .Tx (d .C .LastTxTill (addr .HumanTime ))
mach .Add1 (ss .ScrollToTx , am .A {"Client.txId" : tx .ID })
}
if !skipHistory {
d .hPrependHistory (addr )
d .hUpdateAddressBar ()
d .draw (d .addressBar )
}
return true
}
func (d *Debugger ) hRemoveHistory (clientId string ) {
hist := make ([]*types .MachAddress , 0 )
for i , item := range d .History {
if i <= d .HistoryCursor && d .HistoryCursor > 0 {
d .HistoryCursor --
}
if item .MachId == clientId {
continue
}
hist = append (hist , item )
}
d .History = hist
}
func (d *Debugger ) hPrependHistory (addr *types .MachAddress ) {
d .History = slices .Concat ([]*types .MachAddress {addr }, d .History )
d .hTrimHistory ()
}
func (d *Debugger ) hTrimHistory () {
if d .HistoryCursor > 0 {
rm := d .HistoryCursor
if rm >= len (d .History ) {
rm = len (d .History )
}
d .History = d .History [rm :]
}
d .HistoryCursor = 0
if len (d .History ) > 100 {
d .History = d .History [100 :]
}
}
func (d *Debugger ) hGetClient (machId string ) *Client {
if d .Clients == nil {
return nil
}
c , ok := d .Clients [machId ]
if !ok {
return nil
}
return c
}
func (d *Debugger ) hGetClientTx (
machId , txId string ,
) (*Client , *telemetry .DbgMsgTx ) {
c := d .hGetClient (machId )
if c == nil {
return nil , nil
}
idx := c .TxIndex (txId )
if idx < 0 {
return nil , nil
}
tx := c .Tx (idx )
if tx == nil {
return nil , nil
}
return c , tx
}
func (d *Debugger ) Client () *Client {
var c *Client
<-d .Mach .WhenNot1 (ss .SelectingClient , nil )
d .Mach .Eval ("Client" , func () {
c = d .C
}, nil )
return c
}
func (d *Debugger ) NextTx () *telemetry .DbgMsgTx {
var tx *telemetry .DbgMsgTx
<-d .Mach .WhenNot1 (ss .SelectingClient , nil )
d .Mach .Eval ("NextTx" , func () {
tx = d .hNextTx ()
}, nil )
return tx
}
func (d *Debugger ) hNextTx () *telemetry .DbgMsgTx {
c := d .C
if c == nil {
return nil
}
onLastTx := c .CursorTx1 >= len (c .MsgTxs )
if onLastTx {
return nil
}
return c .MsgTxs [c .CursorTx1 ]
}
func (d *Debugger ) CurrentTx () *telemetry .DbgMsgTx {
var tx *telemetry .DbgMsgTx
<-d .Mach .WhenNot1 (ss .SelectingClient , nil )
d .Mach .Eval ("CurrentTx" , func () {
tx = d .hCurrentTx ()
}, nil )
return tx
}
func (d *Debugger ) hCurrentTx () *telemetry .DbgMsgTx {
c := d .C
if c == nil {
return nil
}
if c .CursorTx1 == 0 || len (c .MsgTxs ) < c .CursorTx1 {
return nil
}
return c .MsgTxs [c .CursorTx1 -1 ]
}
func (d *Debugger ) PrevTx () *telemetry .DbgMsgTx {
var tx *telemetry .DbgMsgTx
<-d .Mach .WhenNot1 (ss .SelectingClient , nil )
d .Mach .Eval ("PrevTx" , func () {
tx = d .hPrevTx ()
}, nil )
return tx
}
func (d *Debugger ) hPrevTx () *telemetry .DbgMsgTx {
c := d .C
if c == nil {
return nil
}
if c .CursorTx1 < 2 {
return nil
}
return c .MsgTxs [c .CursorTx1 -2 ]
}
func (d *Debugger ) hConnectedClients () int {
var conns int
for _ , c := range d .Clients {
if c .Connected .Load () {
conns ++
}
}
return conns
}
func (d *Debugger ) Dispose () {
d .Mach .Dispose ()
<-d .Mach .WhenDisposed ()
logger := d .Opts .DbgLogger
if logger != nil {
if file , ok := logger .Writer ().(*os .File ); ok {
file .Close ()
}
}
}
func (d *Debugger ) Start (
clientID string , txNum int , uiView string , group string ,
) {
d .Mach .Add1 (ss .Start , am .A {
"Client.id" : clientID ,
"cursorTx1" : txNum ,
"dbgView" : uiView ,
"group" : group ,
})
}
func (d *Debugger ) SetFilterLogLevel (lvl am .LogLevel ) {
d .Mach .Eval ("SetFilterLogLevel" , func () {
d .Opts .Filters .LogLevel = lvl
d .Mach .Add1 (ss .ToolToggled , nil )
d .hUpdateSchemaLogGrid ()
d .hRedrawFull (false )
}, nil )
}
func (d *Debugger ) hImportData (filename string ) {
var reader *bufio .Reader
u , err := url .Parse (filename )
if err == nil && u .Host != "" {
resp , err := http .Get (filename )
if err != nil {
d .Mach .AddErr (err , nil )
return
}
reader = bufio .NewReader (resp .Body )
} else {
fr , err := os .Open (filename )
if err != nil {
d .Mach .AddErr (err , nil )
return
}
defer fr .Close ()
reader = bufio .NewReader (fr )
}
brReader := brotli .NewReader (reader )
decoder := gob .NewDecoder (brReader )
var res []*server .Exportable
err = decoder .Decode (&res )
if err != nil {
d .Mach .AddErr (fmt .Errorf ("import failed %w" , err ), nil )
return
}
for _ , data := range res {
id := data .MsgStruct .ID
hash := amhelp .SchemaHash ((*data ).MsgStruct .States )
d .Clients [id ] = newClient (id , id , hash , data )
if d .graph != nil {
err := d .graph .AddClient (data .MsgStruct )
if err != nil {
d .Mach .AddErr (fmt .Errorf ("import failed %w" , err ), nil )
return
}
}
d .Mach .Add1 (ss .InitClient , am .A {"id" : id })
for i := range data .MsgTxs {
d .hParseMsg (d .Clients [id ], i )
}
}
runtime .GC ()
}
func (d *Debugger ) hUpdateToolbar () {
f := fmt .Sprintf
for i , row := range d .toolbarItems {
focused := d .Mach .Is1 (ss .Toolbar1Focused )
switch i {
case 1 :
focused = d .Mach .Is1 (ss .Toolbar2Focused )
case 2 :
focused = d .Mach .Is1 (ss .Toolbar3Focused )
}
for ii , item := range row {
text := ""
_ , sel := d .toolbars [i ].GetSelection ()
esc := cview .Escape
if item .active != nil && item .active () {
if item .activeLabel != nil {
text += f (" [::b]%s[::-]" , esc ("[" +item .activeLabel ()+"]" ))
} else {
text += f (" [::b]%s[::-]" , esc ("[X]" ))
}
} else if item .active == nil && item .icon != "" {
text += f (" [gray]%s[-]%s[gray]%s[-]" , esc ("[" ), item .icon , esc ("]" ))
} else if item .active == nil {
text += f (" [grey][ ][-]" )
} else {
text += f (" [ ]" )
}
if sel != -1 && d .toolbarItems [i ][sel ].id == ToolName (item .id ) &&
focused {
text += "[white]" + item .label
} else if !focused {
text += f ("[%s]%s" , colorHighlight2 , item .label )
} else {
text += f ("%s" , item .label )
}
cell := d .toolbars [i ].GetCell (0 , ii )
cell .SetText (text )
cell .SetTextColor (tcell .ColorWhite )
d .toolbars [i ].SetCell (0 , ii , cell )
}
}
}
func (d *Debugger ) hUpdateAddressBar () {
machId := ""
machConn := false
txId := ""
stepId := ""
if d .C != nil {
machId = d .C .Id
if d .C .CursorTx1 > 0 {
txId = d .C .MsgTxs [d .C .CursorTx1 -1 ].ID
}
if d .C .CursorStep1 > 0 {
stepId = strconv .Itoa (d .C .CursorStep1 )
}
machConn = d .C .Connected .Load ()
}
copyCell := d .addressBar .GetCell (0 , 6 )
copyCell .SetBackgroundColor (tcell .ColorLightGray )
copyCell .SetTextColor (tcell .ColorBlack )
if machId == "" {
copyCell .SetSelectable (false )
copyCell .SetBackgroundColor (tcell .ColorDefault )
} else {
copyCell .SetSelectable (true )
}
pasteCell := d .addressBar .GetCell (0 , 8 )
pasteCell .SetTextColor (tcell .ColorBlack )
pasteCell .SetBackgroundColor (tcell .ColorLightGray )
fwdCell := d .addressBar .GetCell (0 , 2 )
fwdCell .SetBackgroundColor (tcell .ColorLightGray )
fwdCell .SetTextColor (tcell .ColorBlack )
if d .HistoryCursor > 0 {
fwdCell .SetSelectable (true )
} else {
fwdCell .SetSelectable (false )
fwdCell .SetTextColor (tcell .ColorGray )
fwdCell .SetBackgroundColor (tcell .ColorDefault )
}
backCell := d .addressBar .GetCell (0 , 0 )
backCell .SetBackgroundColor (tcell .ColorLightGray )
backCell .SetTextColor (tcell .ColorBlack )
if d .HistoryCursor < len (d .History )-1 {
backCell .SetSelectable (true )
} else {
backCell .SetSelectable (false )
backCell .SetTextColor (tcell .ColorGray )
backCell .SetBackgroundColor (tcell .ColorDefault )
}
if d .clip == nil {
copyCell .SetTextColor (tcell .ColorGrey )
copyCell .SetSelectable (false )
copyCell .SetBackgroundColor (tcell .ColorDefault )
pasteCell .SetTextColor (tcell .ColorGrey )
pasteCell .SetSelectable (false )
pasteCell .SetBackgroundColor (tcell .ColorDefault )
}
machColor := "[grey]"
if machConn {
machColor = "[" + colorActive .String () + "]"
}
addrCell := d .addressBar .GetCell (0 , 4 )
if machId != "" && txId != "" {
s := ""
if stepId != "" {
s = "/" + stepId
}
addrCell .SetText (machColor + "mach://[-][::u]" + machId +
"[::-][grey]/" + txId + s )
} else if machId != "" {
addrCell .SetText (machColor + "mach://[-][::u]" + machId )
} else {
addrCell .SetText ("[grey]mach://[-]" )
}
tags := ""
if machId != "" {
if len (d .C .MsgStruct .Tags ) > 0 {
tags += "[::b]#[::-]" + strings .Join (d .C .MsgStruct .Tags , " [::b]#[::-]" )
}
parentTags := d .hGetParentTags (d .C , nil )
if len (parentTags ) > 0 {
if tags != "" {
tags += " ... "
}
tags += "[::b]#[::-]" + strings .Join (parentTags , " [::b]#[::-]" )
}
}
d .tagsBar .SetText (tags )
}
func (d *Debugger ) hUpdateViews (immediate bool ) {
switch d .Mach .Switch (ss .GroupViews ) {
case ss .MatrixView :
d .hUpdateMatrix ()
d .contentPanels .HidePanel ("tree-log" )
d .contentPanels .HidePanel ("tree-matrix" )
d .contentPanels .ShowPanel ("matrix" )
case ss .TreeMatrixView :
d .hUpdateMatrix ()
d .hUpdateSchemaTree ()
d .contentPanels .HidePanel ("matrix" )
d .contentPanels .HidePanel ("tree-log" )
d .contentPanels .ShowPanel ("tree-matrix" )
case ss .TreeLogView :
fallthrough
default :
d .hUpdateSchemaTree ()
if immediate {
d .Mach .Add1 (ss .UpdateLogScheduled , nil )
} else {
d .Mach .Add1 (ss .UpdateLogScheduled , nil )
}
d .contentPanels .HidePanel ("matrix" )
d .contentPanels .HidePanel ("tree-matrix" )
d .contentPanels .ShowPanel ("tree-log" )
}
}
func (d *Debugger ) hParseMsg (c *Client , idx int ) {
msgTx := c .MsgTxs [idx ]
var sum uint64
for _ , v := range msgTx .Clocks {
sum += v
}
index := c .MsgStruct .StatesIndex
prevTx := &telemetry .DbgMsgTx {}
prevTxParsed := &types .MsgTxParsed {}
if len (c .MsgTxs ) > 1 && idx > 0 {
prevTx = c .MsgTxs [idx -1 ]
prevTxParsed = c .MsgTxsParsed [idx -1 ]
}
fakeTx := &am .Transition {
TimeBefore : prevTx .Clocks ,
TimeAfter : msgTx .Clocks ,
Steps : msgTx .Steps ,
}
after := fakeTx .TimeAfter .Sum (nil )
before := fakeTx .TimeBefore .Sum (nil )
if after < before {
d .Mach .AddErr (fmt .Errorf ("time after < time before" ), nil )
c .MTimeSum = sum
c .MsgTxsParsed = append (c .MsgTxsParsed , &types .MsgTxParsed {TimeSum : sum })
c .LogMsgs = append (c .LogMsgs , make ([]*am .LogEntry , 0 ))
return
}
added , removed , touched := amhelp .GetTransitionStates (fakeTx , index )
msgTxParsed := &types .MsgTxParsed {
TimeSum : sum ,
TimeDiff : sum - prevTxParsed .TimeSum ,
StatesAdded : c .StatesToIndexes (added ),
StatesRemoved : c .StatesToIndexes (removed ),
StatesTouched : c .StatesToIndexes (touched ),
}
if len (msgTx .CalledStates ) > 0 {
msgTx .CalledStatesIdxs = amhelp .StatesToIndexes (index ,
msgTx .CalledStates )
msgTx .MachineID = ""
msgTx .CalledStates = nil
}
for _ , step := range msgTx .Steps {
if step .FromState != "" || step .ToState != "" {
step .FromStateIdx = slices .Index (index , step .FromState )
step .ToStateIdx = slices .Index (index , step .ToState )
step .FromState = ""
step .ToState = ""
}
if step .Data != nil {
step .RelType , _ = step .Data .(am .Relation )
}
}
var isErr bool
for _ , name := range index {
if strings .HasPrefix (name , am .PrefixErr ) && msgTx .Is1 (index , name ) {
isErr = true
break
}
}
if isErr || msgTx .Is1 (index , am .StateException ) {
c .Errors = append ([]int {idx }, c .Errors ...)
}
c .MsgTxsParsed = append (c .MsgTxsParsed , msgTxParsed )
c .MTimeSum = sum
d .hParseMsgLog (c , msgTx , idx )
if d .graph != nil {
d .graph .ParseMsg (c .Id , msgTx )
}
if d .Mach .Is1 (ss .Start ) {
d .Mach .Add1 (ss .BuildingLog , nil )
}
msgTx .CalledStates = amhelp .IndexesToStates (index , msgTx .CalledStatesIdxs )
}
func (d *Debugger ) hIsTxSkipped (c *Client , idx int ) bool {
if !d .filtersActive () {
return false
}
return slices .Index (c .MsgTxsFiltered , idx ) == -1
}
func (d *Debugger ) hFilterTxCursor1 (c *Client , newCursor1 int , back bool ) int {
if !d .filtersActive () {
return newCursor1
}
for {
if newCursor1 < 1 {
return 0
} else if newCursor1 > len (c .MsgTxs ) {
if !d .hIsTxSkipped (c , c .CursorTx1 -1 ) {
return c .CursorTx1
} else {
return 0
}
}
if d .hIsTxSkipped (c , newCursor1 -1 ) {
if back {
newCursor1 --
} else {
newCursor1 ++
}
} else {
break
}
}
return newCursor1
}
func (d *Debugger ) hUpdateTxBars () {
d .currTxBarLeft .Clear ()
d .currTxBarRight .Clear ()
d .nextTxBarLeft .Clear ()
d .nextTxBarRight .Clear ()
if d .Mach .Not (am .S {ss .SelectingClient , ss .ClientSelected }) {
d .currTxBarLeft .SetText ("Listening for connections on " + d .Opts .AddrRpc )
return
}
c := d .C
tx := d .hCurrentTx ()
if tx == nil {
if c == nil || len (c .MsgTxs ) == 0 {
d .currTxBarLeft .SetText ("No transitions yet..." )
} else {
d .currTxBarLeft .SetText ("Initial machine schema" )
}
} else {
var title string
switch d .Mach .Switch (ss .GroupPlaying ) {
case ss .Playing :
title = formatTxBarTitle ("Playing" )
case ss .TailMode :
title += formatTxBarTitle ("Tail" ) + " "
default :
title = formatTxBarTitle ("Paused" ) + " "
}
left , right := d .hGetTxInfo (c .CursorTx1 , tx , c .MsgTxsParsed [c .CursorTx1 -1 ],
title )
d .currTxBarLeft .SetText (left )
d .currTxBarRight .SetText (right )
}
nextTx := d .hNextTx ()
if nextTx != nil && c != nil {
title := "Next "
left , right := d .hGetTxInfo (c .CursorTx1 +1 , nextTx ,
c .MsgTxsParsed [c .CursorTx1 ], title )
d .nextTxBarLeft .SetText (left )
d .nextTxBarRight .SetText (right )
}
}
func (d *Debugger ) hUpdateTimelines () {
c := d .C
if c == nil {
return
}
txCount := len (c .MsgTxs )
nextTx := d .hNextTx ()
d .timelineSteps .SetTitleColor (cview .Styles .PrimaryTextColor )
d .timelineSteps .SetBorderColor (cview .Styles .PrimaryTextColor )
d .timelineSteps .SetFilledColor (cview .Styles .PrimaryTextColor )
if nextTx != nil && !nextTx .Accepted {
d .timelineSteps .SetFilledColor (tcell .ColorGray )
}
if nextTx != nil && c .CursorStep1 == len (nextTx .Steps ) && !nextTx .Accepted {
d .timelineSteps .SetFilledColor (tcell .ColorRed )
}
stepsCount := 0
onLastTx := c .CursorTx1 >= txCount
if !onLastTx {
stepsCount = len (c .MsgTxs [c .CursorTx1 ].Steps )
}
d .timelineTxs .SetMax (max (txCount , 1 ))
d .timelineTxs .SetProgress (c .CursorTx1 )
var title string
if d .filtersActive () {
pos := slices .Index (c .MsgTxsFiltered , c .CursorTx1 -1 ) + 1
if c .CursorTx1 == 0 {
pos = 0
}
title = d .P .Sprintf (" Transition %d / %d [%s]%d / %d[-] " ,
pos , len (c .MsgTxsFiltered ), colorHighlight2 , c .CursorTx1 , txCount )
} else {
title = d .P .Sprintf (" Transition %d / %d " , c .CursorTx1 , txCount )
}
d .timelineTxs .SetTitle (title )
d .timelineTxs .SetEmptyRune (' ' )
d .timelineSteps .SetMax (max (stepsCount , 1 ))
d .timelineSteps .SetProgress (c .CursorStep1 )
d .timelineSteps .SetTitle (fmt .Sprintf (
" Next mutation step %d / %d " , c .CursorStep1 , stepsCount ))
d .timelineSteps .SetEmptyRune (' ' )
}
func (d *Debugger ) hUpdateBorderColor () {
color := colorInactive
if d .Mach .IsErr () {
color = tcell .ColorRed
}
for _ , box := range d .focusable {
box .SetBorderColorFocused (color )
}
}
func (d *Debugger ) hExportData (filename string , snapshot bool ) {
if filename == "" {
log .Printf ("Error: export failed no filename" )
return
}
if len (d .Clients ) == 0 {
log .Printf ("Error: export failed no clients" )
return
}
gobPath := path .Join (d .Opts .OutputDir , filename +".gob.br" )
fw , err := os .Create (gobPath )
if err != nil {
log .Printf ("Error: export failed %s" , err )
return
}
defer fw .Close ()
now := *d .C .Tx (max (0 , d .C .CursorTx1 -1 )).Time
data := make ([]*server .Exportable , len (d .Clients ))
i := 0
for _ , c := range d .Clients {
data [i ] = c .Exportable
if snapshot {
data [i ].MsgTxs = []*telemetry .DbgMsgTx {c .Tx (c .LastTxTill (now ))}
}
i ++
}
brCompress := brotli .NewWriter (fw )
defer brCompress .Close ()
encoder := gob .NewEncoder (brCompress )
err = encoder .Encode (data )
if err != nil {
log .Printf ("Error: export failed %s" , err )
}
}
func (d *Debugger ) hGetTxInfo (txIndex1 int ,
tx *telemetry .DbgMsgTx , parsed *types .MsgTxParsed , title string ,
) (string , string ) {
left := title
right := " "
if tx == nil {
return left , right
}
var prev *telemetry .DbgMsgTx
if txIndex1 > 1 {
prev = d .C .MsgTxs [txIndex1 -2 ]
}
calledStates := tx .CalledStateNames (d .C .MsgStruct .StatesIndex )
left += d .P .Sprintf (" | tx: %d" , txIndex1 )
if parsed .TimeDiff == 0 {
left += " | Time: [gray] 0[-]"
} else {
left += d .P .Sprintf (" | Time: +%d" , parsed .TimeDiff )
}
left += " |"
multi := ""
if len (calledStates ) == 1 && d .C .MsgStruct .States [calledStates [0 ]].Multi {
multi += " multi"
}
if !tx .Accepted {
left += "[grey]"
}
left += fmt .Sprintf (" %s%s: [::b]%s[::-]" , tx .Type , multi ,
strings .Join (calledStates , ", " ))
if !tx .Accepted {
left += "[-]"
}
if tx .IsAuto {
right += "auto | "
}
if tx .IsCheck {
right += "check | "
}
if tx .IsQueued {
right += "[grey]queued[-] | "
} else if !tx .Accepted {
right += "[grey]canceled[-] | "
}
tStamp := tx .Time .Format (timeFormat )
if prev != nil {
prevTStamp := prev .Time .Format (timeFormat )
if idx := findFirstDiff (prevTStamp , tStamp ); idx != -1 {
tStamp = tStamp [:idx ] + "[white]" + tStamp [idx :idx +1 ] + "[grey]" +
tStamp [idx +1 :]
}
}
right += fmt .Sprintf ("add: %d | rm: %d | touch: %3s | [grey]%s" ,
len (parsed .StatesAdded ), len (parsed .StatesRemoved ),
strconv .Itoa (len (parsed .StatesTouched )), tStamp ,
)
return left , right
}
func (d *Debugger ) hCleanOnConnect () bool {
if len (d .Clients ) == 0 {
return false
}
var disconns []*Client
for _ , c := range d .Clients {
if !c .Connected .Load () {
disconns = append (disconns , c )
}
}
if len (disconns ) == len (d .Clients ) {
for _ , c := range d .Clients {
delete (d .Clients , c .Id )
d .hRemoveHistory (c .Id )
}
if d .graph != nil {
d .graph .Clear ()
}
return true
}
return false
}
func (d *Debugger ) hUpdateMatrix () {
if !d .Mach .Any1 (ss .MatrixView , ss .TreeMatrixView ) {
return
}
if d .Mach .Is1 (ss .MatrixRain ) {
d .hUpdateMatrixRain ()
} else {
d .hUpdateMatrixRelations ()
}
}
func (d *Debugger ) hUpdateMatrixRelations () {
d .matrix .Clear ()
d .matrix .SetTitle (" Matrix " )
c := d .C
if c == nil || d .C .CursorTx1 == 0 {
return
}
index := c .MsgStruct .StatesIndex
if c .SelectedGroup != "" {
index = c .MsgSchemaParsed .Groups [c .SelectedGroup ]
}
var tx *telemetry .DbgMsgTx
var prevTx *telemetry .DbgMsgTx
if c .CursorStep1 == 0 {
tx = d .hCurrentTx ()
prevTx = d .hPrevTx ()
} else {
tx = d .hNextTx ()
prevTx = d .hCurrentTx ()
}
steps := tx .Steps
calledStates := tx .CalledStateNames (c .MsgStruct .StatesIndex )
if c .CursorStep1 > 0 {
steps = steps [:c .CursorStep1 ]
}
highlightIndex := -1
var called []int
for i , name := range index {
v := "0"
if slices .Contains (calledStates , name ) {
v = "1"
called = append (called , i )
}
d .matrix .SetCellSimple (0 , i , matrixCellVal (v ))
if slices .Contains (calledStates , name ) {
d .matrix .GetCell (0 , i ).SetAttributes (tcell .AttrBold | tcell .AttrUnderline )
}
if d .C .SelectedState == name {
d .matrix .GetCell (0 , i ).SetBackgroundColor (colorHighlight3 )
highlightIndex = i
}
}
matrixEmptyRow (d , 1 , len (index ), highlightIndex )
sum := 0
for i , name := range index {
var pTick uint64
if prevTx != nil {
pTick = prevTx .Clock (index , name )
}
tick := tx .Clock (index , name )
v := tick - pTick
sum += int (v )
d .matrix .SetCellSimple (2 , i , matrixCellVal (strconv .Itoa (int (v ))))
cell := d .matrix .GetCell (2 , i )
if v == 0 {
cell .SetTextColor (tcell .ColorGrey )
}
if slices .Contains (called , i ) {
cell .SetAttributes (
tcell .AttrBold | tcell .AttrUnderline )
}
if d .C .SelectedState == name {
cell .SetBackgroundColor (colorHighlight3 )
}
}
matrixEmptyRow (d , 3 , len (index ), highlightIndex )
for iRow , target := range index {
for iCol , source := range index {
v := 0
for _ , step := range steps {
if step .GetFromState (c .MsgStruct .StatesIndex ) == source &&
((step .ToStateIdx == -1 && source == target ) ||
step .GetToState (c .MsgStruct .StatesIndex ) == target ) {
v += int (step .Type )
}
strVal := strconv .Itoa (v )
strVal = matrixCellVal (strVal )
d .matrix .SetCellSimple (iRow +4 , iCol , strVal )
cell := d .matrix .GetCell (iRow +4 , iCol )
if d .C .SelectedState == target || d .C .SelectedState == source {
cell .SetBackgroundColor (colorHighlight3 )
}
if v == 0 {
cell .SetTextColor (tcell .ColorGrey )
continue
}
if slices .Contains (called , iRow ) || slices .Contains (called , iCol ) {
cell .SetAttributes (tcell .AttrBold | tcell .AttrUnderline )
} else {
cell .SetAttributes (tcell .AttrBold )
}
}
}
}
title := " Matrix:" + strconv .Itoa (sum ) + " "
if c .CursorTx1 > 0 {
t := strconv .Itoa (int (c .MsgTxsParsed [c .CursorTx1 -1 ].TimeSum ))
title += "Time:t" + t + " "
}
d .matrix .SetTitle (title )
}
func (d *Debugger ) hUpdateMatrixRain () {
if d .Mach .Not1 (ss .MatrixRain ) {
return
}
d .matrix .Clear ()
d .matrix .SetTitle (" Rain " )
c := d .C
if c == nil {
return
}
currTxRow := -1
d .matrix .SetSelectionChangedFunc (func (row , column int ) {
d .Mach .Add1 (ss .MatrixRainSelected , am .A {
"row" : row ,
"column" : column ,
"currTxRow" : currTxRow ,
})
})
d .matrix .SetSelectable (true , true )
index := c .MsgStruct .StatesIndex
if g := c .SelectedGroup ; g != "" {
index = c .MsgSchemaParsed .Groups [g ]
}
tx := d .hCurrentTx ()
prevTx := d .hPrevTx ()
_ , _ , _ , height := d .matrix .GetInnerRect ()
height -= 1
toShow := []int {}
ahead := height / 2
if d .Mach .Is1 (ss .TailMode ) {
ahead = 0
}
cur := c .FilterIndexByCursor1 (c .CursorTx1 )
var curLast int
aheadOk := func (i int , max int ) bool {
return i < len (c .MsgTxsFiltered ) && len (toShow ) <= max
}
for i := cur ; aheadOk (i , ahead ); i ++ {
toShow = append (toShow , c .MsgTxsFiltered [i ])
curLast = i
}
behindOk := func (i int ) bool {
return i >= 0 && i < len (c .MsgTxsFiltered ) && len (toShow ) <= height
}
for i := cur - 1 ; behindOk (i ); i -- {
toShow = slices .Concat ([]int {c .MsgTxsFiltered [i ]}, toShow )
}
for i := curLast + 1 ; aheadOk (i , height ); i ++ {
toShow = append (toShow , c .MsgTxsFiltered [i ])
}
for rowIdx , txIdx1 := range toShow {
row := ""
txIdx1 = txIdx1 + 1
if txIdx1 == c .CursorTx1 {
currTxRow = rowIdx
}
tx := c .MsgTxs [txIdx1 -1 ]
txParsed := c .MsgTxsParsed [txIdx1 -1 ]
calledStates := tx .CalledStateNames (c .MsgStruct .StatesIndex )
for ii , name := range index {
v := "."
sIsErr := strings .HasPrefix (name , "Err" )
if tx .Is1 (index , name ) {
v = "1"
if slices .Contains (txParsed .StatesTouched , ii ) {
v = "2"
}
} else if slices .Contains (txParsed .StatesRemoved , ii ) {
v = "|"
} else if !tx .Accepted && slices .Contains (calledStates , index [ii ]) {
v = "c"
} else if slices .Contains (txParsed .StatesTouched , ii ) {
v = "*"
}
row += v
d .matrix .SetCellSimple (rowIdx , ii , v )
cell := d .matrix .GetCell (rowIdx , ii )
cell .SetSelectable (true )
if !tx .Accepted || v == "." || v == "|" || v == "c" || v == "*" {
cell .SetTextColor (colorHighlight )
}
if slices .Contains (calledStates , name ) {
cell .SetAttributes (tcell .AttrUnderline )
}
if txIdx1 == c .CursorTx1 {
cell .SetBackgroundColor (colorHighlight3 )
} else if d .C .SelectedState == name {
cell .SetBackgroundColor (colorHighlight3 )
}
if (sIsErr || name == am .StateException ) && tx .Is1 (index , name ) {
if tx .Accepted {
cell .SetBackgroundColor (tcell .ColorIndianRed )
} else {
cell .SetBackgroundColor (colorHighlight3 )
}
}
}
tStamp := tx .Time .Format (timeFormat )
tStampFmt := tStamp
if txIdx1 > 1 {
prevTStamp := c .MsgTxs [txIdx1 -2 ].Time .Format (timeFormat )
if idx := findFirstDiff (prevTStamp , tStamp ); idx != -1 {
tStampFmt = tStamp [:idx ] + "[white]" + tStamp [idx :idx +1 ] + "[gray]" +
tStamp [idx +1 :]
}
}
d .matrix .SetCellSimple (rowIdx , len (index ), fmt .Sprintf (
" [gray]%d | %s[-]" , txIdx1 , tStampFmt ))
tailCell := d .matrix .GetCell (rowIdx , len (index ))
if txIdx1 == c .CursorTx1 {
tailCell .SetBackgroundColor (colorHighlight3 )
}
}
diffT := 0
if c .CursorTx1 > 0 {
for _ , name := range index {
var pTick uint64
if prevTx != nil {
pTick = prevTx .Clock (index , name )
}
tick := tx .Clock (index , name )
v := tick - pTick
diffT += int (v )
}
}
title := " Matrix:" + strconv .Itoa (diffT ) + " "
if c .CursorTx1 > 0 {
t := strconv .Itoa (int (c .MsgTxsParsed [c .CursorTx1 -1 ].TimeSum ))
title += "Time:t" + t + " "
}
d .matrix .SetTitle (title )
if d .Mach .Is1 (ss .TailMode ) {
d .matrix .ScrollToEnd ()
}
}
func (d *Debugger ) hUpdateStatusBar () {
c := d .C
if c == nil {
d .statusBar .SetText ("" )
return
}
txt := ""
if c .CursorStep1 > 0 {
tx := c .MsgTxs [c .CursorTx1 ]
step := tx .Steps [c .CursorStep1 -1 ]
txt = step .StringFromIndex (c .MsgStruct .StatesIndex )
}
i := 0
for strings .Contains (txt , "**" ) {
rep := "[::b]"
if i %2 == 1 {
rep = "[::-]"
}
i ++
txt = strings .Replace (txt , "**" , rep , 1 )
}
d .statusBar .SetText (txt )
}
func (d *Debugger ) hGetSidebarCurrClientIdx () int {
if d .C == nil {
return -1
}
i := 0
for _ , item := range d .clientList .GetItems () {
ref := item .GetReference ().(*sidebarRef )
if ref .name == d .C .Id {
return i
}
i ++
}
return -1
}
func (d *Debugger ) hFilterClientTxs () {
if d .C == nil || !d .filtersActive () {
return
}
d .C .MsgTxsFiltered = nil
for i := range d .C .MsgTxs {
match := d .hFilterTx (d .C , i , d .filtersFromStates ())
if match {
d .C .MsgTxsFiltered = append (d .C .MsgTxsFiltered , i )
}
}
}
func (d *Debugger ) filtersFromStates () *OptsFilters {
is := d .Mach .Is1
return &OptsFilters {
SkipCanceledTx : is (ss .FilterCanceledTx ),
SkipAutoTx : is (ss .FilterAutoTx ),
SkipAutoCanceledTx : is (ss .FilterAutoCanceledTx ),
SkipEmptyTx : is (ss .FilterEmptyTx ),
SkipHealthTx : is (ss .FilterHealth ),
SkipQueuedTx : is (ss .FilterQueuedTx ),
SkipOutGroup : is (ss .FilterOutGroup ),
SkipChecks : is (ss .FilterChecks ),
}
}
func (d *Debugger ) filtersActive () bool {
return d .Mach .Any1 (ss .GroupFilters ...)
}
func (d *Debugger ) hFilterTx (c *Client , idx int , filters *OptsFilters ) bool {
tx := c .MsgTxs [idx ]
parsed := c .MsgTxsParsed [idx ]
called := tx .CalledStateNames (c .MsgStruct .StatesIndex )
group := c .SelectedGroup
f := filters
if f .SkipAutoTx && tx .IsAuto {
return false
} else if f .SkipAutoCanceledTx && tx .IsAuto && !tx .Accepted {
return false
}
if f .SkipCanceledTx && !tx .Accepted {
return false
}
if f .SkipQueuedTx && tx .IsQueued {
return false
}
if f .SkipChecks && tx .IsCheck {
return false
}
if f .SkipOutGroup && group != "" {
groupStates := c .MsgSchemaParsed .Groups [group ]
if len (am .StatesShared (called , groupStates )) == 0 {
return false
}
}
if f .SkipEmptyTx && parsed .TimeDiff == 0 && !tx .IsQueued && tx .Accepted {
return false
}
if f .SkipHealthTx {
health := S {ssam .BasicStates .Healthcheck , ssam .BasicStates .Heartbeat }
if len (called ) == 1 && slices .Contains (health , called [0 ]) {
return false
}
}
return true
}
func (d *Debugger ) hScrollToTime (
e *am .Event , hTime time .Time , filter bool ,
) bool {
if d .C == nil {
return false
}
latestTx := d .C .LastTxTill (hTime )
if latestTx == -1 {
return false
}
if filter {
latestTx = d .hFilterTxCursor1 (d .C , latestTx , true )
}
d .hSetCursor1 (e , am .A {
"cursor1" : latestTx ,
"filterBack" : true ,
})
return true
}
func (d *Debugger ) hGetParentTags (c *Client , tags []string ) []string {
parent , ok := d .Clients [c .MsgStruct .Parent ]
if !ok {
return tags
}
tags = slices .Concat (tags , parent .MsgStruct .Tags )
return d .hGetParentTags (parent , tags )
}
func (d *Debugger ) initGraphGen (
snapshot *amgraph .Graph , id string , detailsLvl , numClients int , outDir ,
svgName string , statesAllowlist S ,
) []*amvis .Renderer {
var vizs []*amvis .Renderer
vis := amvis .NewRenderer (snapshot , d .Mach .Log )
amvis .PresetSingle (vis )
switch detailsLvl {
default :
return vizs
case 1 :
vis .RenderNestSubmachines = false
vis .RenderStart = false
vis .RenderInherited = false
vis .RenderPipes = false
vis .RenderHalfPipes = false
vis .RenderHalfConns = false
vis .RenderHalfHierarchy = false
vis .RenderParentRel = false
case 2 :
vis .RenderNestSubmachines = false
vis .RenderStart = true
vis .RenderInherited = true
vis .RenderPipes = false
vis .RenderHalfPipes = false
vis .RenderHalfConns = false
vis .RenderHalfHierarchy = false
case 3 :
vis .RenderNestSubmachines = true
vis .RenderStart = true
vis .RenderInherited = true
vis .RenderPipes = true
}
d .Mach .Log ("rendering graphs lvl %d" , detailsLvl )
vis .RenderMachs = []string {id }
vis .OutputFilename = path .Join (outDir , svgName )
if len (statesAllowlist ) > 0 {
vis .RenderAllowlist = statesAllowlist
}
vizs = append (vizs , vis )
mapPath := fmt .Sprintf ("am-vis-map-%d" , numClients )
if _ , err := os .Stat (mapPath ); err != nil {
vis = amvis .NewRenderer (snapshot , d .Mach .Log )
amvis .PresetMap (vis )
vis .OutputFilename = path .Join (outDir , mapPath )
}
return append (vizs , vis )
}
func (d *Debugger ) hSyncOptsTimelines () {
switch d .Opts .Timelines {
case 0 :
d .Mach .Add (S {ss .TimelineTxHidden , ss .TimelineStepsHidden }, nil )
case 1 :
d .Mach .Add1 (ss .TimelineStepsHidden , nil )
d .Mach .Remove1 (ss .TimelineTxHidden , nil )
case 2 :
d .Mach .Remove (S {ss .TimelineStepsHidden , ss .TimelineTxHidden }, nil )
}
}
func (d *Debugger ) diagramsRender (
e *am .Event , shot *amgraph .Graph , id string , details , clients int ,
outDir string , svgName string , states S ,
) {
ctx := d .Mach .NewStateCtx (ss .DiagramsRendering )
if ctx .Err () != nil {
return
}
d .Mach .EvRemove1 (e , ss .DiagramsScheduled , nil )
vizs := d .initGraphGen (shot , id , details , clients , outDir , svgName , states )
pool := amhelp .Pool (2 )
for _ , vis := range vizs {
pool .Go (func () error {
return vis .GenDiagrams (ctx )
})
}
err := pool .Wait ()
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
d .diagramLink (outDir , svgName )
if ctx .Err () != nil {
return
}
source := path .Join (outDir , "am-vis-map.svg" )
target := fmt .Sprintf ("am-vis-map-%d.svg" , clients )
_ = os .Remove (source )
err = os .Symlink (target , source )
if ctx .Err () != nil {
return
}
d .Mach .EvAddErr (e , err , nil )
d .Mach .EvAdd1 (e , ss .DiagramsReady , nil )
}
func (d *Debugger ) diagramLink (outDir string , svgName string ) {
source := path .Join (outDir , "am-vis.svg" )
target := fmt .Sprintf ("%s.svg" , svgName )
_ = os .Remove (source )
err := os .Symlink (target , source )
d .Mach .AddErr (err , nil )
}
func (d *Debugger ) diagramsMemCache (
e *am .Event , id string , cache *goquery .Document , tx *telemetry .DbgMsgTx ,
outDir , svgName string ,
) {
svgPath := path .Join (outDir , svgName +".svg" )
ctx := d .Mach .NewStateCtx (ss .DiagramsRendering )
if ctx .Err () != nil {
return
}
d .Mach .EvRemove1 (e , ss .DiagramsScheduled , nil )
states := d .C .MsgStruct .StatesIndex
sel := amvis .Fragment {
MachId : id ,
States : states ,
Active : tx .ActiveStates (states ),
}
err := amvis .UpdateCache (ctx , svgPath , cache , &sel )
if ctx .Err () != nil {
return
}
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
d .diagramLink (outDir , svgName )
if ctx .Err () != nil {
return
}
d .Mach .EvAdd1 (e , ss .DiagramsReady , nil )
}
func (d *Debugger ) diagramsFileCache (
e *am .Event , id string , tx *telemetry .DbgMsgTx , lvl int , outDir ,
svgName string ,
) {
defer d .Mach .PanicToErrState (ss .ErrDiagrams , nil )
svgPath := path .Join (outDir , svgName +".svg" )
ctx := d .Mach .NewStateCtx (ss .DiagramsRendering )
if ctx .Err () != nil {
return
}
d .Mach .EvRemove1 (e , ss .DiagramsScheduled , nil )
file , err := os .Open (svgPath )
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
cache , err := goquery .NewDocumentFromReader (file )
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
states := d .C .MsgStruct .StatesIndex
sel := amvis .Fragment {
MachId : id ,
States : states ,
}
if tx != nil {
sel .Active = tx .ActiveStates (states )
}
err = amvis .UpdateCache (ctx , svgPath , cache , &sel )
if ctx .Err () != nil {
return
}
if err != nil {
d .Mach .EvAddErrState (e , ss .ErrDiagrams , err , nil )
return
}
d .diagramLink (outDir , svgName )
if ctx .Err () != nil {
return
}
d .Mach .EvAdd1 (e , ss .DiagramsReady , am .A {
"Diagram.cache" : cache ,
"Diagram.id" : id ,
"Diagram.lvl" : lvl ,
})
}
func (d *Debugger ) getFocusColor () tcell .Color {
color := cview .Styles .MoreContrastBackgroundColor
if d .Mach .IsErr () {
color = tcell .ColorRed
}
return color
}
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 .