// TODO ExceptionState: separate error screen with stack trace

package debugger

import (
	
	
	
	
	
	
	
	
	
	
	
	

	
	
	
	
	
	
	

	
	

	amhelp 
	am 
	
	ss 
	
)

// TODO Enter

func ( *Debugger) ( *am.Event) {
	,  := .Args["Client.id"].(string)
	,  := .Args["cursorTx1"].(int)
	,  := .Args["group"].(string)
	,  := .Args["dbgView"].(string)

	// cview TUI app
	.App = cview.NewApplication()
	if .Opts.Screen != nil {
		.App.SetScreen(.Opts.Screen)
	}

	// forceful race solving
	.App.SetBeforeDrawFunc(func( tcell.Screen) bool {
		// dont draw while transitioning
		 := .Mach.Transition() == nil
		if ! {
			// reschedule this repaint
			// d.Mach.Log("postpone draw")
			.repaintPending.Store(true)
			return true
		}

		// mark as in progress
		.drawing.Store(true)
		return false
	})
	.App.SetAfterDrawFunc(func( tcell.Screen) {
		.drawing.Store(false)
	})
	.App.SetAfterResizeFunc(func( int,  int) {
		go func() {
			time.Sleep(time.Millisecond * 300)
			.Mach.Add1(ss.Resized, nil)
		}()
	})

	// init the rest
	.P = message.NewPrinter(language.English)
	.hBindKeyboard()
	.hInitUiComponents()
	.hInitLayout()
	.hUpdateFocusable()
	if .Opts.EnableMouse {
		.App.EnableMouse(true)
	}
	if .Opts.ShowReader {
		.Mach.Add1(ss.LogReaderEnabled, nil)
	}

	// default filters TODO sync filter from CLI
	 := S{ss.FilterChecks}
	if .Opts.Filters.SkipOutGroup {
		 = append(, ss.FilterOutGroup)
	}
	.Mach.Add(, nil)

	 := .Mach.NewStateCtx(ss.Start)

	// draw in a goroutine
	go func() {
		if .Err() != nil {
			return // expired
		}
		.Mach.PanicToErr(nil)

		.App.SetRoot(.LayoutRoot, true)
		 := .App.Run()
		if  != nil {
			.Mach.AddErr(, nil)
		}

		.Dispose()
	}()

	// post-start ops
	go func() {
		if .Err() != nil {
			return // expired
		}

		// initial view from CLI
		switch  {
		case "tree-matrix":
			.Mach.Add1(ss.TreeMatrixView, nil)
		case "matrix":
			.Mach.Add1(ss.MatrixView, nil)
		}

		// go directly to Ready when data empty
		if len(.Clients) <= 0 {
			.Mach.Add1(ss.Ready, nil)
			return
		}

		// init imported data
		.buildClientList(-1)
		 := maps.Keys(.Clients)
		if  != "" {
			// partial match available client IDs
			for ,  := range  {
				if strings.Contains(, ) {
					 = 
					break
				}
			}
		}
		// default selected ID
		if !slices.Contains(, ) {
			 = [0]
		}
		.hPrependHistory(&types.MachAddress{MachId: })
		// TODO timeout
		.Mach.Add1(ss.SelectingClient, am.A{
			"Client.id": ,
			"group":     ,
		})
		<-.Mach.When1(ss.ClientSelected, nil)

		if .Err() != nil {
			return // expired
		}

		if  != 0 {
			.Mach.Add1(ss.ScrollToTx, am.A{"cursorTx1": })
		}

		.Mach.Add1(ss.Ready, nil)
	}()
}

func ( *Debugger) ( *am.Event) {
	if .App.GetScreen() != nil {
		.App.Stop()
	}
}

func ( *Debugger) ( *am.Event) {
	.heartbeatT = time.NewTicker(heartbeatInterval)

	// late options
	// TODO migrate args from Start() method
	if .Opts.ViewNarrow {
		.Mach.EvAdd1(, ss.NarrowLayout, nil)
	}
	.hSyncOptsTimelines()
	if .Opts.OutputDiagrams > 0 {
		.Mach.EvAdd1(, ss.DiagramsScheduled, nil)
	}
	if .Opts.ViewRain {
		.Mach.EvAdd1(, ss.MatrixRain, nil)
	}
	if .Opts.TailMode {
		.Mach.EvAdd1(, ss.TailMode, nil)
	}

	// TODO merge parsing with addr bar
	if ,  := types.ParseMachUrl(.Opts.MachUrl);  == nil {
		// TODO race
		go .hGoToMachAddress(, false)
	}

	// unblock
	go func() {
		for {
			select {
			case <-.heartbeatT.C:
				.Mach.Add1(ss.Heartbeat, nil)

			case <-.Mach.Ctx().Done():
				.heartbeatT.Stop()
			}
		}
	}()
}

func ( *Debugger) ( *am.Event) {
	.heartbeatT.Stop()
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.Heartbeat, nil)
	go amhelp.AskEvAdd1(, .Mach, ss.GcMsgs, nil)
}

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["state"].(string)
	return 
}

func ( *Debugger) ( *am.Event) {
	.C.SelectedState = .Args["state"].(string)
	.lastSelectedState = .C.SelectedState

	switch .Mach.Switch(ss.GroupViews) {

	case ss.TreeLogView:
		.hUpdateSchemaTree()

	case ss.TreeMatrixView:
		.hUpdateSchemaTree()
		.hUpdateMatrix()

	case ss.MatrixView:
		.hUpdateMatrix()
	}

	.hUpdateStatusBar()
}

func ( *Debugger) ( *am.Event) {
	if .C != nil {
		.C.SelectedState = ""
	}
	.hUpdateSchemaTree()
	.hUpdateStatusBar()
}

func ( *Debugger) ( *am.Event) {
	if .playTimer == nil {
		.playTimer = time.NewTicker(playInterval)
	} else {
		// TODO dont reset if resuming after switching clients
		.playTimer.Reset(playInterval)
	}

	// initial play step
	if .Mach.Is1(ss.TimelineStepsFocused) {
		.Mach.Add1(ss.FwdStep, nil)
	} else {
		.Mach.Add1(ss.Fwd, nil)
	}
	.hUpdateToolbar()

	 := .Mach.NewStateCtx(ss.Playing)
	go func() {
		for .Err() == nil {
			select {
			case <-.Done(): // expired

			case <-.playTimer.C:

				if .Mach.Is1(ss.TimelineStepsFocused) {
					.Mach.Add1(ss.FwdStep, nil)
				} else {
					.Mach.Add1(ss.Fwd, nil)
				}
			}
		}
	}()
}

func ( *Debugger) ( *am.Event) {
	.playTimer.Stop()
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	// TODO stop scrolling the log when coming from TailMode (confirm)
	.hUpdateTxBars()
	.draw()
}

func ( *Debugger) ( *am.Event) {
	.hSetCursor1(, am.A{
		"cursor1":    len(.C.MsgTxs),
		"filterBack": true,
	})
	.hUpdateMatrix()
	.hUpdateClientList()
	.hUpdateToolbar()
	// needed bc tail mode if carried over via SelectingClient
	.hRedrawFull(true)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateMatrix()
	.hUpdateToolbar()
	.hRedrawFull(true)
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.Redraw, nil)
	,  := .Args["immediate"].(bool)
	.hRedrawFull()
}

// ///// FWD / BACK

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.UserFwd, nil)
}

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["amount"].(int)
	 = max(, 1)
	return .C.CursorTx1+ <= len(.C.MsgTxs)
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.Fwd, nil)
	 := .C

	,  := .Args["amount"].(int)
	 = max(, 1)

	.hSetCursor1(, am.A{
		"cursor1": .CursorTx1 + ,
	})
	if .Mach.Is1(ss.Playing) && .CursorTx1 == len(.MsgTxs) {
		.Mach.Remove1(ss.Playing, nil)
	}

	// sidebar for errs
	.hUpdateClientList()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.UserBack, nil)
}

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["amount"].(int)
	 = max(, 1)
	return .C.CursorTx1- >= 0
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.Back, nil)

	,  := .Args["amount"].(int)
	 = max(, 1)

	.hSetCursor1(, am.A{
		"cursor1":    .C.CursorTx1 - ,
		"filterBack": true,
	})

	// sidebar for errs
	.hUpdateClientList()
	.hRedrawFull(false)
}

// ///// STEP BACK / FWD

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.UserFwdStep, nil)
}

func ( *Debugger) ( *am.Event) bool {
	 := .hNextTx()
	if  == nil {
		return false
	}
	return .C.CursorStep1 < len(.Steps)+1
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.FwdStep, nil)

	// next tx
	 := .hNextTx()
	// scroll to the next tx
	if .C.CursorStep1 == len(.Steps) {
		.Mach.Add1(ss.Fwd, nil)
		return
	}
	.C.CursorStep1++

	.hHandleTStepsScrolled()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.UserBackStep, nil)
}

func ( *Debugger) ( *am.Event) bool {
	return .C.CursorStep1 > 0 || .C.CursorTx1 > 0
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.BackStep, nil)

	// wrap if there's a prev tx
	if .C.CursorStep1 <= 0 {
		.hSetCursor1(, am.A{
			"cursor1": .C.CursorTx1 - 1,
		})

		.Mach.Add1(ss.UpdateLogScheduled, nil)
		if  := .hNextTx();  != nil {
			.C.CursorStep1 = len(.Steps)
		}

	} else {
		.C.CursorStep1--
	}

	.updateClientList()
	.hHandleTStepsScrolled()
	.hRedrawFull(false)
}

// TODO move
func ( *Debugger) () {
	// TODO merge with a CursorStep setter
	 := .C.CursorStep1 != 0

	if  {
		.Mach.Add1(ss.TimelineStepsScrolled, nil)
	} else {
		.Mach.Remove1(ss.TimelineStepsScrolled, nil)
	}
}

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.toolbars[0].SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.toolbars[0].SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.toolbars[1].SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.toolbars[1].SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.toolbars[2].SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.toolbars[2].SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.addressBar.SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.addressBar.SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.treeGroups.SetBackgroundColor(.getFocusColor())
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.treeGroups.SetBackgroundColor(cview.Styles.PrimitiveBackgroundColor)
	.hUpdateToolbar()
}

// ///// CONNECTION

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["msg_struct"].(*telemetry.DbgMsgStruct)
	,  := .Args["conn_id"].(string)
	if ! || ! || .ID == "" {
		.Mach.Log("Error: msg_struct malformed\n")
		return false
	}

	return true
}

func ( *Debugger) ( *am.Event) {
	// initial structure data
	 := .Args["msg_struct"].(*telemetry.DbgMsgStruct)
	 := .Args["conn_id"].(string)
	var  *Client

	// cleanup removes all previous clients if all are disconnected
	 := false
	if .Opts.CleanOnConnect {
		// remove old clients
		 = .hCleanOnConnect()
	}

	// update existing client
	if ,  := .Clients[.ID];  {
		if .ConnId != "" && .ConnId ==  {
			.Mach.Log("schema changed for %s", .ID)
			// TODO use MsgStructPatch
			// TODO keep old revisions
			.MsgStruct = 
			 = 
			.ParseSchema()

		} else {
			.Mach.Log("client %s already exists, overriding", .ID)
		}
	}

	// create a new client
	if  == nil {
		 := &server.Exportable{
			MsgStruct: ,
		}
		 = newClient(.ID, , amhelp.SchemaHash(.States), )
		.Connected.Store(true)
		.Clients[.ID] = 
	}

	// re-select the last group TODO broken
	if  := .lastSelectedGroup;  != "" {
		if ,  := .MsgStruct.Groups[];  {
			.SelectedGroup = 
		}
	}

	if ! {
		.buildClientList(-1)
	}

	// rebuild the UI in case of a cleanup or connect under the same ID
	if  || (.C != nil && .C.Id == .ID) {
		// select the new (and only) client
		.C = 
		.log.Clear()
		.hUpdateTimelines()
		.hUpdateTxBars()
		.hUpdateBorderColor()
		.buildClientList(0)
		// initial build of the schema tree
		.hBuildSchemaTree()
		.hUpdateTreeGroups()
		.hUpdateViews(false)
	}

	// remove the last active client if over the limit
	// TODO prioritize disconns
	if len(.Clients) > maxClients {
		var (
			 time.Time
			   string
		)
		// TODO get time from msgs
		for ,  := range .Clients {
			 := .LastActive()
			if .After() ||  == "" {
				 = 
				 = 
			}
		}
		.Mach.Add1(ss.RemoveClient, am.A{"Client.id": })
	}

	// if only 1 client connected, select it
	// if the only client in total, select it
	if len(.Clients) == 1 || (.Opts.SelectConnected &&
		.hConnectedClients() == 1) {

		.Mach.Add1(ss.SelectingClient, am.A{
			"Client.id": .ID,
			// mark the origin
			"from_connected": true,
		})
		.hPrependHistory(&types.MachAddress{MachId: .ID})

		// re-select the state
		if .lastSelectedState != "" {
			.Mach.Add1(ss.StateNameSelected, am.A{"state": .lastSelectedState})
			// TODO Keep in StateNameSelected behind a flag
			.hSelectTreeState(.lastSelectedState)
		}
	}

	// first client, tail mode
	if len(.Clients) == 1 {
		.Mach.Add1(ss.TailMode, nil)
	}

	// graph
	if .graph != nil {
		_ = .graph.AddClient()
		// TODO errors, check for dups, enable once stable
		// if err != nil {
		// d.Mach.AddErr(err, nil)
		// }
	}
	.Mach.Add1(ss.InitClient, am.A{"id": .ID})

	.draw()
}

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["conn_id"].(string)
	if ! {
		.Mach.Log("Error: DisconnectEvent malformed\n")
		return false
	}

	return true
}

func ( *Debugger) ( *am.Event) {
	 := .Args["conn_id"].(string)
	for ,  := range .Clients {
		if .ConnId != "" && .ConnId ==  {
			// mark as disconnected
			.Connected.Store(false)
			.Mach.Log("client %s disconnected", .Id)
			break
		}
	}

	.hUpdateBorderColor()
	.hUpdateAddressBar()
	.hUpdateClientList()
	.draw()
}

// ///// CLIENTS

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["msgs_tx"].([]*telemetry.DbgMsgTx)
	,  := .Args["conn_ids"].([]string)
	return  && 
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.ClientMsg, nil)

	// TODO make it async via a dedicated goroutine, pushing results to
	//  async multi state ClientMsgDone (if possible)

	 := .Args["msgs_tx"].([]*telemetry.DbgMsgTx)
	 := .Args["conn_ids"].([]string)
	 := .Mach

	// GC in progress - save msgs and parse on next ClientMsgState
	if .Is1(ss.GcMsgs) {
		.msgsDelayed = append(.msgsDelayed, ...)
		.msgsDelayedConns = append(.msgsDelayedConns, ...)

		return
	}

	// parse pending msgs, if any
	if len(.msgsDelayed) > 0 {
		 = slices.Concat(.msgsDelayed, )
		 = slices.Concat(.msgsDelayedConns, )
		.msgsDelayed = nil
		.msgsDelayedConns = nil
	}

	 := false
	 := false
	 := false
	for ,  := range  {

		// TODO check tokens
		 := .MachineID
		 := .Clients[]
		if ,  := .Clients[]; ! {
			.Mach.Log("Error: client not found: %s\n", )
			continue
		}

		if .MsgStruct == nil {
			.Mach.Log("Error: schema missing for %s, ignoring tx\n", )
			continue
		}

		// verify it's from the same client
		if .ConnId != [] {
			.Mach.Log("Error: conn_id mismatch for %s, ignoring tx\n", )
			continue
		}

		// append and parse the msg
		// TODO scalable storage (support filtering)
		 := len(.MsgTxs)
		.MsgTxs = append(.MsgTxs, )
		.hParseMsg(, )
		 := .hFilterTx(, , .filtersFromStates())
		if  {
			.MsgTxsFiltered = append(.MsgTxsFiltered, )
		}

		if  == .C {
			 = true
			 := .hAppendLogEntry()
			if  != nil {
				.Mach.Log("Error: log append %s\n", )
				// d.Mach.AddErr(err, nil)
				return
			}
			if .Mach.Is1(ss.TailMode) {
				 = true
			}

			// update Tx info on the first Tx
			if len(.MsgTxs) == 1 {
				 = true
			}
		}
	}

	// UI updates for the selected client
	if  {
		// force the latest tx
		.hSetCursor1(, am.A{
			"cursor1":    len(.C.MsgTxs),
			"filterBack": true,
		})
		// sidebar for errs
		.hUpdateViews(false)
	}

	// update Tx info on the first Tx
	if  ||  {
		.hUpdateTxBars()
	}

	// timelines always change
	.updateClientList()
	.hUpdateTimelines()
	.hUpdateMatrix()
	.hUpdateAddressBar()

	if  {
		.draw()
	}
}

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["Client.id"].(string)
	,  := .Clients[]

	return  &&  != "" && 
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.RemoveClient, nil)
	 := .Args["Client.id"].(string)
	 := .Clients[]

	// clean up
	delete(.Clients, )
	.hRemoveHistory(.Id)

	// if currently selected, switch to the first one
	if  == .C {
		for  := range .Clients {
			.Mach.Add1(ss.SelectingClient, am.A{"Client.id": })
			break
		}
		// if last client, unselect
		if len(.Clients) == 0 {
			.Mach.Remove1(ss.ClientSelected, nil)
		}
		.buildClientList(-1)
	} else {
		.buildClientList(.clientList.GetCurrentItemIndex() - 1)
	}

	.draw()
}

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["group"].(string)
	if ! {
		return false
	}

	// extract
	, _, _ = strings.Cut(, ":")

	_,  = .C.MsgSchemaParsed.Groups[]
	return  != "" &&  != .C.SelectedGroup && 
}

func ( *Debugger) ( *am.Event) {
	 := .Args["group"].(string)
	 := .C

	if  == "all" {
		.SelectedGroup = ""
	} else {
		.SelectedGroup = strings.Split(, ":")[0]
	}
	.lastSelectedGroup = .SelectedGroup
	.hBuildSchemaTree()
	.hUpdateSchemaTree()
	go amhelp.AskEvAdd1(, .Mach, ss.DiagramsScheduled, nil)
	.Mach.EvAdd(, am.S{ss.ToolToggled, ss.BuildingLog}, am.A{
		// TODO typed args
		"filterTxs":     true,
		"logRebuildEnd": len(.MsgTxs),
	})
}

// TODO SelectingClientSelectingClient (for?)

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["Client.id"].(string)
	// same client
	if .C != nil &&  == .C.Id {
		return false
	}
	// does the client exist?
	,  := .Clients[]

	return len(.Clients) > 0 &&  && 
}

func ( *Debugger) ( *am.Event) {
	// TODO support tx ID
	 := .Args["Client.id"].(string)
	,  := .Args["group"].(string)
	,  := .Args["from_connected"].(bool)
	 := slices.Contains(.Transition().StatesBefore(), ss.Playing)

	if .Clients[] == nil {
		// TODO handle err, remove state
		.Mach.Log("Error: client not found: %s\n", )

		return
	}

	 := .Mach.NewStateCtx(ss.SelectingClient)
	// select the new default client
	.C = .Clients[]
	// re-feed the whole log and pass the context to allow cancelation
	 := len(.C.LogMsgs)
	// remain in TailMode after the selection
	 := slices.Contains(.Transition().StatesBefore(), ss.TailMode)
	.C.SelectedGroup = 

	// TODO extract SelectingClientFiltered
	go func() {
		if .Err() != nil {
			return // expired
		}

		// start with prepping the data
		.hFilterClientTxs()

		// scroll to the same place as the prev client
		// TODO continue in SelectingClientFilteredState
		 := false
		if ! {
			 = .hScrollToTime(, .lastScrolledTxTime, true)
		}

		// or scroll to the last one
		if ! {
			.Mach.Eval("SelectingClientState", func() {
				.hSetCursor1(, am.A{
					"cursor1":    len(.C.MsgTxs),
					"filterBack": true,
				})
			}, )
			if .Err() != nil {
				return // expired
			}

		} else {
			// [hSetCursor1] triggers DiagramsScheduled, so do we
			go amhelp.AskEvAdd1(, .Mach, ss.DiagramsScheduled, nil)
		}

		// scroll client list item into view
		 := .hGetSidebarCurrClientIdx()
		,  := .clientList.GetOffset()
		, , ,  := .clientList.GetInnerRect()
		if - >  {
			.clientList.SetCurrentItem()
			.updateClientList()
		}

		// rebuild the whole log
		 := am.S{ss.ClientSelected, ss.BuildingLog}
		if  {
			 = append(, ss.TailMode)
		}

		.Mach.Add(, am.A{
			"ctx":            ,
			"from_connected": ,
			"from_playing":   ,
			"logRebuildEnd":  ,
		})
	}()
}

func ( *Debugger) ( *am.Event) {
	 := .Args["ctx"].(context.Context)
	,  := .Args["from_connected"].(bool)
	,  := .Args["from_playing"].(bool)
	if .Err() != nil {
		.Mach.Log("Error: context expired\n")
		return // expired
	}

	// catch up with new log msgs
	for  := .logRebuildEnd;  < len(.C.LogMsgs); ++ {
		 := .hAppendLogEntry()
		if  != nil {
			.Mach.Log("Error: log rebuild %s\n", )
		}
	}

	// initial build of the schema tree
	.hBuildSchemaTree()
	.hUpdateTreeGroups()
	.hUpdateTimelines()
	.hUpdateTxBars()
	.hUpdateClientList()
	if .Mach.Is1(ss.TreeLogView) || .Mach.Is1(ss.TreeMatrixView) {
		.hUpdateSchemaTree()
	}

	// update views
	if .Mach.Is1(ss.TreeLogView) {
		.Mach.Add1(ss.UpdateLogScheduled, nil)
	}
	if .Mach.Is1(ss.MatrixView) || .Mach.Is1(ss.TreeMatrixView) {
		.hUpdateMatrix()
	}

	// first client connected, set tail mode
	if  && len(.Clients) == 1 {
		.Mach.Add1(ss.TailMode, nil)
	} else if  {
		.Mach.Add1(ss.Playing, nil)
	}

	// re-select the state
	if .lastSelectedState != "" {
		.Mach.Add1(ss.StateNameSelected, am.A{"state": .lastSelectedState})
		// TODO Keep in StateNameSelected behind a flag
		.hSelectTreeState(.lastSelectedState)
	}

	.hUpdateBorderColor()
	.hUpdateAddressBar()
	.draw()
}

func ( *Debugger) ( *am.Event) {
	 := .Mach.Index1(ss.SelectingClient)
	// clean up, except when switching to SelectingClient
	if !.Mutation().IsCalled() {
		.C = nil
	}

	.log.Clear()
	.treeRoot.ClearChildren()
	.hRedrawFull(true)
}

func ( *Debugger) ( *am.Event) {
	// re-render for mem stats
	.hUpdateHelpDialog()
	.hUpdateToolbar()
	// TODO use Visibility instead of SendToFront
	.LayoutRoot.SendToFront("main")
	.LayoutRoot.SendToFront(DialogHelp)
	.Mach.Add1(ss.AfterFocus, am.A{"cview.Primitive": .helpDialog})
}

func ( *Debugger) ( *am.Event) {
	 := .Transition()
	 := am.DiffStates(ss.GroupDialog, .TargetStates())
	if len() == len(ss.GroupDialog) {
		// all dialogs closed, show main
		.LayoutRoot.SendToFront("main")
		.Mach.Add1(ss.UpdateFocus, nil)
		.hUpdateToolbar()

		// TODO prev focus, read self-log?
		.focusDefault()
	}
}

func ( *Debugger) ( *am.Event) {
	.hUpdateToolbar()
	// TODO use Visibility instead of SendToFront
	.LayoutRoot.SendToFront("main")
	.LayoutRoot.SendToFront(DialogExport)
	.exportDialog.SetFocus(0)
	// TODO fix focus the first field
	.hUpdateFocusable()
	.Mach.Add1(ss.DialogFocused, nil)
	.Mach.Add1(ss.UpdateFocus, nil)
	// d.exportDialog.GetForm().GetFormItem(0).Focus(nil)
	// d.App.SetFocus(d.exportDialog.GetForm().GetFormItem(0))
	// d.draw()
}

func ( *Debugger) ( *am.Event) {
	 := am.DiffStates(ss.GroupDialog, .Transition().TargetStates())
	if len() == len(ss.GroupDialog) {
		// all dialogs closed, show main
		.hUpdateFocusable()
		.LayoutRoot.SendToFront("main")
		.Mach.Add1(ss.UpdateFocus, nil)
		.hUpdateToolbar()

		// TODO prev focus
		.focusDefault()
	}
}

func ( *Debugger) ( *am.Event) {
	.hDrawViews()
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.hDrawViews()
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.hDrawViews()
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) {
	.hDrawViews()
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) bool {
	 := .Transition().TimeIndexAfter()

	return .Any1(ss.TreeMatrixView, ss.MatrixView) &&
		.Is1(ss.ClientSelected)
}

func ( *Debugger) ( *am.Event) {
	.hDrawViews()
	.hUpdateToolbar()
}

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["cursorTx1"].(int)
	,  := .Args["Client.txId"].(string)
	 := .C

	return  != nil && ( && .TxIndex() > -1 ||
		 && len(.MsgTxs) > ) &&  >= 0
}

// ScrollToTxState scrolls to a specific transition (cursor position 1-based).
func ( *Debugger) ( *am.Event) {
	defer .Mach.EvRemove1(, ss.ScrollToTx, nil)

	,  := .Args["cursorTx1"].(int)
	,  := .Args["cursorStep1"].(int)
	,  := .Args["trimHistory"].(bool)
	,  := .Args["Client.txId"].(string)
	if  {
		 = .C.TxIndex() + 1
	}

	.hSetCursor1(, am.A{
		"cursor1":     ,
		"cursorStep1": ,
		"trimHistory": ,
	})
	.updateClientList()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) bool {
	// always allow to exit
	if .Transition().TimeIndexAfter().Not1(ss.Start) {
		return true
	}

	return !.Opts.ViewNarrow
}

func ( *Debugger) ( *am.Event) {
	.hUpdateLayout()
	.buildClientList(-1)
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateLayout()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["cursorStep1"].(int)
	 := .C
	return  != nil &&  > 0 && .hNextTx() != nil
}

// ScrollToStepState scrolls to a specific transition (cursor position 1-based).
func ( *Debugger) ( *am.Event) {
	// TODO multi?
	.Mach.EvRemove1(, ss.ScrollToStep, nil)

	 := .Args["cursorStep1"].(int)
	 := .hNextTx()

	if  > len(.Steps) {
		 = len(.Steps)
	}
	.C.CursorStep1 = 

	.hHandleTStepsScrolled()
	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["ToolName"].(ToolName)
	return 
}

func ( *Debugger) ( *am.Event) {
	// TODO split the state into an async one
	// TODO refac to FilterToggledState
	 := .Args["ToolName"].(ToolName)

	// tool is a filter and needs re-filter txs
	 := false

	switch  {
	// TODO move logic after toggle to handlers

	case toolFilterCanceledTx:
		.Mach.EvToggle1(, ss.FilterCanceledTx, nil)
		 = true

	case toolFilterQueuedTx:
		.Mach.EvToggle1(, ss.FilterQueuedTx, nil)
		 = true

	case toolFilterAutoTx:
		.Mach.EvToggle1(, ss.FilterAutoTx, nil)
		switch .Mach.Switch(am.S{ss.FilterAutoTx, ss.FilterAutoCanceledTx}) {
		case ss.FilterAutoTx:
			.Mach.EvAdd1(, ss.FilterAutoCanceledTx, nil)
		case ss.FilterAutoCanceledTx:
			.Mach.EvRemove1(, ss.FilterAutoTx, nil)
		default:
			.Mach.EvAdd1(, ss.FilterAutoTx, nil)
		}
		 = true

	case toolFilterEmptyTx:
		.Mach.EvToggle1(, ss.FilterEmptyTx, nil)
		 = true

	case toolFilterHealth:
		.Mach.EvToggle1(, ss.FilterHealth, nil)
		 = true

	case toolFilterOutGroup:
		.Mach.EvToggle1(, ss.FilterOutGroup, nil)
		 = true

	case toolFilterChecks:
		.Mach.EvToggle1(, ss.FilterChecks, nil)
		 = true

	case ToolLogTimestamps:
		.Mach.EvToggle1(, ss.LogTimestamps, nil)
		 = true

	case ToolFilterTraces:
		.Mach.EvToggle1(, ss.FilterTraces, nil)

	case toolLog:
		.Opts.Filters.LogLevel = (.Opts.Filters.LogLevel + 1) % 6
		.hUpdateSchemaLogGrid()

	case toolDiagrams:
		.Opts.OutputDiagrams = (.Opts.OutputDiagrams + 1) % 4
		.Mach.EvAdd1(, ss.DiagramsScheduled, nil)

	case toolTimelines:
		.Opts.Timelines = (.Opts.Timelines + 1) % 3
		.hSyncOptsTimelines()

	case toolReader:
		if .Mach.Is1(ss.LogReaderEnabled) &&
			.Mach.Any1(ss.MatrixView, ss.TreeMatrixView) {

			.Mach.EvAdd1(, ss.TreeLogView, nil)
		} else if .Mach.Not1(ss.LogReaderEnabled) {
			.Mach.EvAdd1(, ss.LogReaderEnabled, nil)
		} else {
			.Mach.EvRemove1(, ss.LogReaderEnabled, nil)
		}

	case toolRain:
		.Mach.EvAdd1(, ss.ToolRain, nil)

	case toolHelp:
		.Mach.EvToggle1(, ss.HelpDialog, nil)

	case toolPlay:
		if .Mach.Is1(ss.Paused) {
			.Mach.EvAdd1(, ss.Playing, nil)
		} else {
			.Mach.EvAdd1(, ss.Paused, nil)
		}

	case toolTail:
		.Mach.EvToggle1(, ss.TailMode, nil)

	case toolPrev:
		.Mach.EvAdd1(, ss.UserBack, nil)

	case toolNext:
		.Mach.EvAdd1(, ss.UserFwd, nil)

	case toolNextStep:
		.Mach.EvAdd1(, ss.UserFwdStep, nil)

	case toolPrevStep:
		.Mach.EvAdd1(, ss.UserBackStep, nil)

	case toolJumpPrev:
		// TODO state
		go .hJumpBackKey(nil)

	case toolJumpNext:
		// TODO state
		go .hJumpFwdKey(nil)

	case toolFirst:
		.hToolFirstTx()

	case toolLast:
		.hToolLastTx()

	case toolExpand:
		// TODO refresh toolbar on focus changes, reflect expansion state
		.hToolExpand()

	case toolMatrix:
		.toolMatrix()

	case toolExport:
		.Mach.EvToggle1(, ss.ExportDialog, nil)
	}

	// TODO typed args
	.Mach.EvAdd1(, ss.ToolToggled, am.A{"filterTxs": })
}

func ( *Debugger) ( *am.Event) {
	defer .Mach.Remove1(ss.ToolToggled, nil)
	,  := .Args["filterTxs"].(bool)

	if  {
		.hFilterClientTxs()
	}

	// TODO skip on dialogs
	if .C != nil {

		// TODO scroll the log to prev position

		// stay on the last one
		if .Mach.Is1(ss.TailMode) {
			.hSetCursor1(, am.A{
				"cursor1":    len(.C.MsgTxs),
				"filterBack": true,
			})
		}

		// rebuild the whole log to reflect the UI changes
		.Mach.EvAdd1(, ss.BuildingLog, am.A{
			"logRebuildEnd": len(.C.MsgTxs),
		})
		// TODO optimization: param to avoid this
		.Mach.Add1(ss.UpdateLogScheduled, nil)

		if  {
			.hSetCursor1(, am.A{
				"cursor1":    .C.CursorTx1,
				"filterBack": true,
			})
		}
	}

	.updateClientList()
	.hUpdateToolbar()
	.hUpdateTimelines()
	.hUpdateMatrix()
	.hUpdateFocusable()
	.hUpdateTxBars()
	.draw()
}

func ( *Debugger) ( *am.Event) {
	,  := .Args["Client.id"].(string)
	,  := .Args["cursorTx1"].(int)
	 := .Mach.NewStateCtx(ss.SwitchingClientTx)

	go func() {
		if .Err() != nil {
			return // expired
		}

		if .C != nil && .C.Id !=  {
			// TODO async helper
			 := .Mach.WhenTicks(ss.ClientSelected, 2, )
			.Mach.Add1(ss.SelectingClient, am.A{"Client.id": })
			<-
			if .Err() != nil {
				return // expired
			}
		}

		 := .Mach.WhenTicks(ss.ScrollToTx, 2, )
		.Mach.Add1(ss.ScrollToTx, am.A{
			"cursorTx1":   ,
			"trimHistory": true,
		})
		<-
		if .Err() != nil {
			return // expired
		}

		.Mach.Add1(ss.SwitchedClientTx, nil)
	}()
}

func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.SwitchedClientTx, nil)
}

// ScrollToMutTxState scrolls to a transition which mutated the passed state,
// If fwd is true, it scrolls forward, otherwise backwards.
func ( *Debugger) ( *am.Event) {
	.Mach.Remove1(ss.ScrollToMutTx, nil)

	// TODO validate in Enter
	,  := .Args["state"].(string)
	,  := .Args["fwd"].(bool)

	 := .C
	if  == nil {
		return
	}
	 := -1
	if  {
		 = 1
	}

	for  := .CursorTx1 + ;  > 0 &&  < len(.MsgTxs)+1;  =  +  {

		 :=  - 1
		 := .MsgTxsParsed[]
		 := .MsgTxs[]

		// check mutations and canceled
		if !slices.Contains(.IndexesToStates(.StatesAdded), ) &&
			!slices.Contains(.IndexesToStates(.StatesRemoved), ) &&
			!slices.Contains(.CalledStateNames(.MsgStruct.StatesIndex), ) {

			continue
		}

		// skip filtered out
		if .filtersActive() && !slices.Contains(.MsgTxsFiltered, ) {
			continue
		}

		// scroll to this tx
		.Mach.Add1(ss.ScrollToTx, am.A{
			"cursorTx1":   ,
			"trimHistory": true,
		})
		break
	}
}

func ( *Debugger) ( *am.Event) bool {
	// ignore eval timeouts, but log them
	 := am.ParseArgs(.Args)
	if errors.Is(.Err, am.ErrEvalTimeout) {
		if !.IsCheck {
			.Mach.Log(.Err.Error())
		}

		return false
	}

	return true
}

// ExceptionState creates a log file with the error and stack trace, after
// calling the super exception handler.
func ( *Debugger) ( *am.Event) {
	.ExceptionHandler.ExceptionState()
	 := am.ParseArgs(.Args)

	.hUpdateBorderColor()

	// create / append the err log file
	 := fmt.Sprintf("\n\n%s\n%s\n\n%s", time.Now(), .Err, .ErrTrace)
	 := filepath.Join(.Opts.OutputDir, "am-dbg-err.log")
	,  := os.OpenFile(, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644)
	if  != nil {
		.Mach.Log("Error: %s\n", )
		return
	}
	_,  = .Write([]byte())
	if  := .Close();  != nil &&  == nil {
		.Mach.Log("Error: %s\n", )
		return
	}
}

func ( *Debugger) ( *am.Event) bool {
	return AllocMem() > uint64(.Opts.MaxMemMb)*1024*1024
}

func ( *Debugger) ( *am.Event) {
	// TODO GC log reader entries
	// TODO GC tx steps before GCing transitions
	defer .Mach.Remove1(ss.GcMsgs, nil)
	 := .Mach.NewStateCtx(ss.GcMsgs)

	// unblock

	// get oldest clients
	 := maps.Values(.Clients)
	slices.SortFunc(, func(,  *Client) int {
		if len(.MsgTxs) == 0 || len(.MsgTxs) == 0 {
			return 0
		}
		if (*.MsgTxs[0].Time).After(*.MsgTxs[0].Time) {
			return 1
		} else {
			return -1
		}
	})

	 := AllocMem()
	.Mach.Log(.P.Sprintf("GC mem: %d bytes\n", ))

	// check TTL of client log msgs >lvl 2
	// TODO remember the tip of cleaning (date) and binary find it, then
	//  continue
	for ,  := range  {
		for ,  := range .LogMsgs {
			 := .MsgTxs[].Time
			if .Add(.Opts.Log2Ttl).After(time.Now()) {
				continue
			}

			// TODO optimize
			var  []*am.LogEntry
			for ,  := range  {
				if  == nil {
					continue
				}
				if .Level <= am.LogChanges {
					 = append(, )
				}
			}

			// override
			.LogMsgs[] = 
		}
	}

	runtime.GC()
	 := AllocMem()
	if  >  {
		.Mach.Log(.P.Sprintf("GC logs shaved %d bytes\n", -))
	}

	 := 0
	for AllocMem() > uint64(.Opts.MaxMemMb)*1024*1024 {
		if .Err() != nil {
			.Mach.Log("GC: context expired")
			break
		}
		if  > 100 {
			.Mach.AddErr(errors.New("too many GC rounds"), nil)
			break
		}
		.Mach.Log("GC tx round %d", )
		++

		// delete a half per client
		for ,  := range  {
			if .Err() != nil {
				.Mach.Log("GC: context expired")
				break
			}
			 := len(.MsgTxs) / 2
			.MsgTxs = .MsgTxs[:]
			.MsgTxsParsed = .MsgTxsParsed[:]
			.LogMsgs = .LogMsgs[:]

			// empty cache
			.ClearCache()
			// TODO GC c.logReader
			// TODO refresh c.errors (extract from hParseMsg)s

			// adjust the current client
			if .C ==  {

				// rebuild the whole log
				.Mach.Add1(ss.BuildingLog, am.A{
					"logRebuildEnd": len(.MsgTxs) - 1,
				})
				.CursorTx1 = int(math.Max(0, float64(.CursorTx1-)))
				// re-filter
				if .filtersActive() {
					.hFilterClientTxs()
				}
			}

			// delete small clients
			if len(.MsgTxs) < msgMaxThreshold {
				.Mach.Add1(ss.RemoveClient, am.A{"Client.id": .Id})
			} else {
				.MTimeSum = 0
				for ,  := range .MsgTxsParsed {
					.MTimeSum += .TimeSum
				}
			}
		}

		runtime.GC()
	}
	 := AllocMem()
	if  >  {
		.Mach.Log(.P.Sprintf("GC in total shaved %d bytes", -))
	}

	.hRedrawFull(false)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.Mach.Add1(ss.UpdateFocus, nil)
}

func ( *Debugger) ( *am.Event) {
	.hUpdateSchemaLogGrid()
	.Mach.Add1(ss.UpdateFocus, nil)
}

func ( *Debugger) ( *am.Event) bool {
	,  := .Args["cursor1"].(int)
	return 
}

// TODO remove?
func ( *Debugger) ( *am.Event) {
	.hSetCursor1(, .Args)
}

// hSetCursor1 sets both the tx and steps cursors, 1-based.
func ( *Debugger) ( *am.Event,  am.A) {
	// TODO typed args...
	 := ["cursor1"].(int)
	,  := ["cursorStep1"].(int)
	,  := ["skipHistory"].(bool)
	,  := ["trimHistory"].(bool)
	,  := ["filterBack"].(bool)

	// TODO optimize for no-change?
	.C.CursorTx1 = .hFilterTxCursor1(.C, , )
	// reset the step timeline
	// TODO validate
	.C.CursorStep1 = 

	// TODO dont create 2 entries on the first mach change
	if .HistoryCursor == 0 && ! {
		// add current mach if needed
		if len(.History) > 0 && .History[0].MachId != .C.Id {
			.hPrependHistory(.hGetMachAddress())
		}
		// keeping the current tx as history head
		if  := .C.Tx(.C.CursorTx1 - 1);  != nil {
			// dup the current machine if tx differs
			if len(.History) > 1 && .History[1].MachId == .C.Id &&
				.History[1].TxId != .ID {

				.hPrependHistory(.History[0].Clone())
			}
			if len(.History) > 0 {
				.History[0].TxId = .ID
			}
		}
		.hTrimHistory()

	} else if  {
		.hTrimHistory()
	}
	.hHandleTStepsScrolled()

	// debug
	// d.Opts.DbgLogger.Printf("HistoryCursor: %d\n", d.HistoryCursor)
	// d.Opts.DbgLogger.Printf("History: %v\n", d.History)

	.lastScrolledTxTime = time.Time{}
	if  > 0 {
		 := .hCurrentTx()
		if  != nil {
			.lastScrolledTxTime = *.Time
		}

		// tx file
		if .Opts.OutputTx {
			 := .C.MsgStruct.StatesIndex
			_, _ = .txListFile.WriteAt([]byte(.TxString()), 0)
		}
	}

	.Mach.EvRemove1(, ss.TimelineStepsScrolled, nil)
	// optional diagrams
	go amhelp.AskEvAdd1(, .Mach, ss.DiagramsScheduled, nil)
}

// func (d *Debugger) CursorSetState(e *am.Event) {
// }

func ( *Debugger) ( *am.Event) bool {
	// TODO refuse on too many ErrDiagrams, remove ErrDiagrams in ErrDiagramsState
	return .C != nil && .Opts.OutputDiagrams > 0
}

func ( *Debugger) ( *am.Event) {
	// TODO cancel rendering on:
	//  - client change
	//  - details change
	//  - but not on tx change (wait until completed)
	.Mach.EvAdd1(, ss.DiagramsRendering, nil)
}

func ( *Debugger) ( *am.Event) bool {
	return .Opts.OutputDiagrams > 0 && .C != nil
}

func ( *Debugger) ( *am.Event) {
	 := .Opts.OutputDiagrams
	 := path.Join(.Opts.OutputDir, "diagrams")
	 := .C
	 := .hCurrentTx()
	 := fmt.Sprintf("%s-%d-%s", .Id, , .SchemaHash)

	// state groups
	var  S
	if  := .SelectedGroup;  != "" {
		 = .MsgSchemaParsed.Groups[]
		 := strings.ReplaceAll(strings.ReplaceAll(, "-", ""), " ", "")
		 = fmt.Sprintf("%s-%s-%d-%s",
			.Id, strings.ToLower(), , .SchemaHash)
	}
	 := filepath.Join(, +".svg")

	// output dir
	if  := os.MkdirAll(, 0o755);  != nil {
		.Mach.EvAddErrState(, ss.ErrDiagrams,
			fmt.Errorf("create output dir: %w", ), nil)
	}

	// cached?

	// mem cache
	if .cache.diagramId == .Id && .cache.diagramLvl ==  {
		 := .cache.diagramDom
		go .diagramsMemCache(, .Id, , , , )
		return

		// file cache, move to mem cache
	} else if ,  := os.Stat();  == nil {
		go .diagramsFileCache(, .Id, , .cache.diagramLvl, , )
		return
	}

	// no cache - render

	// clone the current graph TODO optimize
	,  := .graph.Clone()
	if  != nil {
		.Mach.EvAddErrState(, ss.ErrDiagrams, , nil)
		return
	}

	// unblock
	go .diagramsRender(, , .Id, , len(.Clients), , , )
}

func ( *Debugger) ( *am.Event) {
	defer .Mach.EvRemove1(, ss.DiagramsReady, nil)

	// update cache
	// TODO typed args
	,  := .Args["Diagram.cache"].(*goquery.Document)
	if  {
		.cache.diagramDom = 
		.cache.diagramLvl = .Args["Diagram.lvl"].(int)
		.cache.diagramId = .Args["Diagram.id"].(string)
	}

	// render a fresher one, if scheduled
	.genGraphsLast = time.Now()
	if .Mach.Is1(ss.DiagramsScheduled) {
		.Mach.EvRemove1(, ss.DiagramsScheduled, nil)
		.Mach.EvAdd1(, ss.DiagramsRendering, nil)
	}
}

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.ClientListVisible)
	// TODO via relations
	.Mach.Add1(ss.UpdateFocus, nil)
	.buildClientList(-1)
	go func() {
		if !amhelp.Wait(, sidebarUpdateDebounce) {
			return
		}
		.drawClientList()
	}()
}

func ( *Debugger) ( *am.Event) {
	// TODO via relations
	.Mach.Add1(ss.UpdateFocus, nil)
}

func ( *Debugger) ( *am.Event) {
	// handled in TimelineStepsHiddenState
	if .Machine().Is1(ss.TimelineStepsHidden) {
		return
	}

	.hUpdateLayout()
}

func ( *Debugger) ( *am.Event) {
	.hUpdateLayout()
}

func ( *Debugger) ( *am.Event) {
	.hUpdateLayout()
}

func ( *Debugger) ( *am.Event) {
	.hUpdateLayout()
}

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.UpdateFocus)
	.hUpdateFocusable()
	.hUpdateBorderColor()

	var  cview.Primitive
	var  *cview.Box
	// change focus (or not) when changing view types
	switch .Mach.Switch(ss.GroupFocused) {
	default:
		 = .clientList
		 = .clientList.Box
	case ss.AddressFocused:
		 = .addressBar
		 = .addressBar.Box
	case ss.TreeFocused:
		 = .tree
		 = .tree.Box
	case ss.LogFocused:
		 = .log
		 = .log.Box
	case ss.LogReaderFocused:
		 = .logReader
		 = .logReader.Box
	case ss.MatrixFocused:
		 = .matrix
		 = .matrix.Box
	case ss.TimelineTxsFocused:
		 = .timelineTxs
		 = .timelineTxs.Box
	case ss.TimelineStepsFocused:
		 = .timelineSteps
		 = .timelineSteps.Box
	case ss.Toolbar1Focused:
		 = .toolbars[0]
		 = .toolbars[0].Box
	case ss.Toolbar2Focused:
		 = .toolbars[1]
		 = .toolbars[1].Box
	case ss.Toolbar3Focused:
		 = .toolbars[2]
		 = .toolbars[2].Box
	case ss.DialogFocused:
		switch {
		case .Mach.Is1(ss.HelpDialog):
			 = .helpDialog
			 = .helpDialog.Box
		case .Mach.Is1(ss.ExportDialog):
			 = .exportDialog
			 = .exportDialog.Box
		}
	}

	// move focus if invalid
	if !slices.Contains(.focusable, ) {
		// TODO take prev el from GroupFocused (ordered visually)
		// TODO fall back to addr bar if not client list
		 = .clientList
	}

	// unblock bc of locks
	// TODO race fix locks
	go func() {
		if .Err() != nil {
			return // expired
		}

		// TODO called on init without focusable elements
		.focusManager.Focus()
	}()
}

func ( *Debugger) ( *am.Event) bool {
	// TODO typed params
	,  := .Args["cview.Primitive"]
	if ! {
		return false
	}

	,  := .hBoxFromPrimitive()

	// skip when focus impossible
	return slices.Contains(.focusable, )
}

func ( *Debugger) ( *am.Event) {
	// TODO typed params
	 := .Args["cview.Primitive"]

	,  := .hBoxFromPrimitive()
	// d.Mach.Log("focusing %s", state)
	 := .focusManager.SetFocusIndex(slices.Index(.focusable, ))
	if  != nil {
		.Mach.AddErr(, nil)
		return
	}
	.Mach.Add1(, nil)

	// update the log highlight on focus change
	if .Mach.Is1(ss.TreeLogView) && .Mach.Not1(ss.LogReaderFocused) {
		.Mach.Add1(ss.UpdateLogScheduled, nil)
	}

	.hUpdateClientList()
	.hUpdateStatusBar()
}

func ( *Debugger) ( *am.Event) {
	if .Mach.Is1(ss.MatrixRain) {
		.Mach.Add1(ss.TreeLogView, nil)
	} else {
		.Mach.Add(am.S{ss.MatrixRain, ss.TreeMatrixView}, nil)
		// TODO force redraw to get rect size, not ideal
		.redrawCallback = func() {
			time.Sleep(16 * time.Millisecond)
			.Mach.Eval("ToolRainState", func() {
				.hDrawViews()
			}, nil)
		}
	}
}

// AnyEnter prevents most of mutations during a UI redraw (and vice versa)
// forceful race solving
func ( *Debugger) ( *am.Event) bool {
	// always pass network traffic
	 := .Mach
	 := .Mutation()
	 := .CalledIndex(ss.Names)
	 := S{
		ss.ClientMsg, ss.ConnectEvent, ss.DisconnectEvent,
		am.StateException,
	}
	if .Any1(...) {
		return true
	}

	// dont allow mutations while drawing, pull 10 times
	 := .HandlerTimeout / 10
	 := 100
	// compensate extended timeouts
	if amhelp.IsDebug() {
		 = 10 * time.Millisecond
	}
	for .IsValid() {
		 := !.drawing.Load()
		if  {
			// ok
			return true
		}

		// delay, but avoid the race detector which gets stuck here
		if !.Opts.DbgRace {
			time.Sleep()
		}

		--
		if  <= 0 {
			// not ok
			break
		}
	}

	// prepend this mutation to the queue and try again TODO loop guard
	// d.Mach.Log("postpone mut")
	go .PrependMut()

	return false
}

// AnyState is a global final handler
func ( *Debugger) ( *am.Event) {
	 := .Transition()

	// redraw on auto states
	// TODO this should be done better
	if .IsAuto() && .IsAccepted.Load() {
		.repaintPending.Store(false)
		.hUpdateTxBars()
		.draw()
	} else if .repaintPending.Swap(false) {
		.draw()
	}
}

func ( *Debugger) ( *am.Event) {
	// TODO TYPED PARAMS
	 := .Args["*http.Request"].(*http.Request)
	 := .Args["http.ResponseWriter"].(http.ResponseWriter)
	 := .Args["doneChan"].(chan struct{})
	defer close()

	 := .RequestURI
	switch {

	// diagram viewer
	case  == "/":
		fallthrough
	case  == "/diagrams/mach":
		 := string(visualizer.HtmlDiagram)
		 = strings.ReplaceAll(, "localhost:6831", .Opts.AddrHttp)
		,  := .Write([]byte())
		.Mach.EvAddErrState(, ss.ErrWeb, , nil)

	// default svg symlink
	case strings.HasPrefix(, "/diagrams/mach.svg"):
		 := filepath.Join(.Opts.OutputDir, "diagrams", "am-vis.svg")
		,  := os.ReadFile()
		.Mach.EvAddErrState(, ss.ErrWeb, , nil)
		if  != nil {
			return
		}
		_,  = .Write()
		.Mach.EvAddErrState(, ss.ErrWeb, , nil)
	}
}

func ( *Debugger) ( *am.Event) {
	// TODO TYPED PARAMS
	 := .Args["*websocket.Conn"].(*websocket.Conn)
	 := .Args["*http.Request"].(*http.Request)
	 := .Args["doneChan"].(chan struct{})
	 := make(chan struct{})

	// unblock
	go func() {
		for {
			// wait for diagrams start
			select {
			case <-:
				close()
				return
			case <-.Mach.When1(ss.DiagramsScheduled, nil):
			}

			// wait for diagrams ready
			select {
			case <-:
				close()
				return
			// TODO loop over WSs in DiagramsReady. no goroutine per each
			case <-.Mach.When1(ss.DiagramsReady, nil):
				// msg
				 := .Write(.Context(), websocket.MessageText,
					[]byte("refresh"))
				.Mach.EvAddErrState(, ss.ErrWeb, , nil)
				if  != nil {
					return
				}
			}
		}
	}()

	go func() {
		for {
			// msgType, msg, err := ws.Read(r.Context())
			, ,  := .Read(.Context())
			if  != nil {
				if websocket.CloseStatus() != -1 {
					.Mach.Log("websocket closed")
				} else {
					 = fmt.Errorf("websocket read: %w", )
					.Mach.EvAddErrState(, ss.ErrWeb, , nil)
				}

				// close up
				close()
				return
			}
		}
	}()
}

func ( *Debugger) ( *am.Event) {
	// show empty when showing canceled
	.Mach.Remove1(ss.FilterEmptyTx, nil)
}

func ( *Debugger) ( *am.Event) {
	// show empty when showing queued
	.Mach.Remove1(ss.FilterEmptyTx, nil)
}

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.MatrixRainSelected)
	 := .Args["row"].(int)
	 := .Args["column"].(int)
	 := .Args["currTxRow"].(int)
	 := .C
	 := .MsgStruct.StatesIndex
	// _, _, _, height := d.matrix.GetInnerRect()

	// select state name
	if  >= 0 &&  < len() {
		.Mach.Add1(ss.StateNameSelected, am.A{
			"state": [],
		})
	}

	// scroll to another?
	if  ==  ||  == -1 {
		return
	}

	 :=  - 
	 := .FilterIndexByCursor1(.CursorTx1) + 
	if  == -1 {
		return
	}

	 = .MsgTxsFiltered[] + 1

	// unblock
	go func() {
		if .Err() != nil {
			return // expired
		}

		// scroll
		if am.Canceled == amhelp.Add1Sync(, .Mach, ss.ScrollToTx, am.A{
			"cursorTx1":   ,
			"trimHistory": true,
		}) {
			return
		}

		// update layout
		.Mach.Eval("MatrixRainSelectedState", func() {
			.hUpdateMatrixRain()
			.updateClientList()
			.hRedrawFull(false)
		}, )
	}()
}

func ( *Debugger) ( *am.Event) {
	 := .Mach.NewStateCtx(ss.Resized)

	.lastResize = .Mach.Time(nil).Sum(nil)
	.hUpdateNarrowLayout()

	// rebuild log
	if .Mach.Not1(ss.ClientSelected) {
		return
	}

	// TODO loose logRebuildEnd and include as relation
	.Mach.Add1(ss.BuildingLog, am.A{"logRebuildEnd": len(.C.MsgTxs)})
	go func() {
		<-.Mach.When1(ss.LogBuilt, )
		if .Err() != nil {
			return // expired
		}
		// force a redraw TODO bug?
		.draw()
	}()
}