mukan-ignite/ignite/cmd/bubblemodel/chain_serve.go
Mukan Erkin Törük c32551b6f7
Some checks failed
Docs Deploy / build_and_deploy (push) Has been cancelled
Generate Docs / cli (push) Has been cancelled
Generate Config Doc / cli (push) Has been cancelled
Go formatting / go-formatting (push) Has been cancelled
Check links / markdown-link-check (push) Has been cancelled
Integration / pre-test (push) Has been cancelled
Integration / test on (push) Has been cancelled
Integration / status (push) Has been cancelled
Lint / Lint Go code (push) Has been cancelled
Test / test (ubuntu-latest) (push) Has been cancelled
refactor: replace all github.com upstream refs with git.cw.tr/mukan-network
2026-05-11 03:36:24 +03:00

278 lines
7.8 KiB
Go

package cmdmodel
import (
"context"
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea"
"git.cw.tr/mukan-network/mukan-ignite/ignite/internal/announcements"
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/colors"
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/icons"
cliuimodel "git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/cliui/model"
"git.cw.tr/mukan-network/mukan-ignite/ignite/pkg/events"
)
const (
maxStatusEvents = 7
)
const (
stateChainServeStarting uint = iota
stateChainServeRunning
stateChainServeRebuilding
stateChainServeQuitting
)
var (
msgStopServe = colors.Faint("Press the 'q' key to stop serve")
msgWaitingFix = colors.Info("Waiting for a fix before retrying...")
)
type Context interface {
// Context returns the current context.
Context() context.Context
// SetContext updates the context with a new one.
SetContext(context.Context)
}
// NewChainServe returns a new UI model for the chain serve command.
func NewChainServe(mCtx Context, bus events.Provider, cmd tea.Cmd) ChainServe {
// Initialize a context and cancel function to stop execution
ctx, quit := context.WithCancel(mCtx.Context())
// Update the context to allow stopping by using the 'q' key
mCtx.SetContext(ctx)
return ChainServe{
cmd: cmd,
quit: quit,
startModel: cliuimodel.NewStatusEvents(bus, maxStatusEvents),
runModel: cliuimodel.NewEvents(bus),
rebuildModel: cliuimodel.NewStatusEvents(bus, maxStatusEvents),
quitModel: cliuimodel.NewEvents(bus),
}
}
// ChainServe defines a UI model for the chain serve command.
type ChainServe struct {
cmd tea.Cmd
quit context.CancelFunc
state uint // Keeps track of the model/view being displayed
broken bool // True when blockchain app's source code has issues
error error // Critical error returned during command execution
// Model definitions for the chain serve views
startModel cliuimodel.StatusEvents
runModel cliuimodel.Events
rebuildModel cliuimodel.StatusEvents
quitModel cliuimodel.Events
}
// Init is the first function that will be called.
// It returns a batch command that listen events and also runs the blockchain app.
func (m ChainServe) Init() tea.Cmd {
// On initialization wait for status events and start serving the blockchain
return tea.Batch(m.startModel.WaitEvent, m.cmd)
}
// Update is called when a message is received.
// It handles messages and executes the logic that updates the model.
func (m ChainServe) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if checkQuitKeyMsg(msg) {
m.state = stateChainServeQuitting
}
switch msg := msg.(type) {
case cliuimodel.QuitMsg:
return m.processQuitMsg(msg)
case cliuimodel.ErrorMsg:
return m.processErrorMsg(msg)
case tea.KeyMsg:
return m.processKeyMsg(msg)
case cliuimodel.EventMsg:
return m.processEventMsg(msg)
default:
return m.updateCurrentModel(msg)
}
}
// View renders the UI after every update.
func (m ChainServe) View() string {
if m.error != nil {
return fmt.Sprintf("%s %s\n", icons.NotOK, colors.Error(m.error.Error()))
}
var view strings.Builder
switch m.state {
case stateChainServeStarting:
view.WriteString(m.renderStartView())
case stateChainServeRunning:
view.WriteString(m.renderRunView())
case stateChainServeRebuilding:
view.WriteString(m.renderRebuildView())
case stateChainServeQuitting:
view.WriteString(m.renderQuitView())
}
if m.state != stateChainServeQuitting {
view.WriteString(m.renderActions())
}
return cliuimodel.FormatView(view.String())
}
func (m ChainServe) updateCurrentModel(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch m.state {
case stateChainServeStarting:
m.startModel, cmd = m.startModel.Update(msg)
case stateChainServeRunning:
m.runModel, cmd = m.runModel.Update(msg)
case stateChainServeRebuilding:
m.rebuildModel, cmd = m.rebuildModel.Update(msg)
case stateChainServeQuitting:
m.quitModel, cmd = m.quitModel.Update(msg)
}
return m, cmd
}
func (m ChainServe) processQuitMsg(cliuimodel.QuitMsg) (tea.Model, tea.Cmd) {
return m, tea.Quit
}
func (m ChainServe) processErrorMsg(msg cliuimodel.ErrorMsg) (tea.Model, tea.Cmd) {
m.error = msg.Error
return m, tea.Quit
}
func (m ChainServe) processKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if checkQuitKeyMsg(msg) {
// Cancel the context to signal stop
m.quit()
}
return m, nil
}
func (m ChainServe) processEventMsg(msg cliuimodel.EventMsg) (tea.Model, tea.Cmd) {
// When an error event is received it means there is an issue with
// the blockchain app's source code that the user must fix.
m.broken = msg.Group == events.GroupError
// UI responds to key press or mouse events by default but we use
// events and the events bus to interact with the UI during execution.
// Check if the state must be changed to switch to a different view.
switch m.state {
case stateChainServeStarting:
// Start view displays status events until the blockchain is running or an
// error event is received in which case it displays the run view with an
// error traceback and waits until the issue is fixed.
// When the status finish event is not an error it means that the blockchain
// started successfully and the run view is displayed.
if msg.ProgressIndication == events.IndicationFinish {
m.state = stateChainServeRunning
}
case stateChainServeRunning:
// Run view shows account addresses, API URLs and the paths required to
// have a context on the running blockchain app and waits for errors or
// changes in the blockchain app source code.
// If an error event is received during run it means that there is an error
// in the app source code in which case the error message and traceback are
// displayed until the code is fixed, or otherwise when an status event is
// received it means that the code changed so the app must be rebuilt.
if m.broken {
// Clear events to only display the error received with the last event message
m.runModel.ClearEvents()
} else if msg.InProgress() {
// When a status event is received during run it means something
// changed in the source code which triggers the blockchain rebuild.
m.runModel.ClearEvents()
m.state = stateChainServeRebuilding
}
case stateChainServeRebuilding:
// Rebuild view is similar to run view but only displayed when the source
// code changes and the blockchain is rebuilt.
// When the status finish event is not an error it means that the blockchain
// was rebuilt successfully and the run view is displayed.
if msg.ProgressIndication == events.IndicationFinish {
m.rebuildModel.ClearEvents()
m.state = stateChainServeRunning
}
}
// Update the model that is being displayed
return m.updateCurrentModel(msg)
}
func (m ChainServe) renderActions() string {
return fmt.Sprintf("\n%s\n", msgStopServe)
}
func (m ChainServe) renderStartView() string {
return m.startModel.View()
}
func (m ChainServe) renderRunView() string {
var view strings.Builder
if !m.broken {
view.WriteString("Blockchain is running\n\n")
}
view.WriteString(m.runModel.View())
if m.broken {
fmt.Fprintf(&view, "\n%s\n", msgWaitingFix)
}
return view.String()
}
func (m ChainServe) renderRebuildView() string {
var view strings.Builder
if !m.broken {
view.WriteString("Changes detected, restarting...\n\n")
}
view.WriteString(m.rebuildModel.View())
if m.broken {
fmt.Fprintf(&view, "\n%s\n", msgWaitingFix)
}
return view.String()
}
func (m ChainServe) renderQuitView() string {
var view strings.Builder
// Display the events received during quit
if s := m.quitModel.View(); s != "" {
view.WriteString(s)
view.WriteRune('\n')
}
fmt.Fprintf(&view, "%s %s\n\n", icons.Info, colors.Info("Stopped"))
view.WriteString(announcements.Fetch())
return view.String()
}
func checkQuitKeyMsg(m tea.Msg) bool {
msg, ok := m.(tea.KeyMsg)
if !ok {
return false
}
key := msg.String()
return key == "q" || key == "ctrl+c"
}