tiki/internal/bootstrap/init.go
2026-01-17 11:08:53 -05:00

193 lines
6 KiB
Go

package bootstrap
import (
"context"
"log/slog"
"github.com/rivo/tview"
"github.com/boolean-maybe/tiki/config"
"github.com/boolean-maybe/tiki/controller"
"github.com/boolean-maybe/tiki/internal/app"
"github.com/boolean-maybe/tiki/internal/background"
"github.com/boolean-maybe/tiki/model"
"github.com/boolean-maybe/tiki/plugin"
"github.com/boolean-maybe/tiki/store"
"github.com/boolean-maybe/tiki/store/tikistore"
"github.com/boolean-maybe/tiki/view"
"github.com/boolean-maybe/tiki/view/header"
)
// BootstrapResult contains all initialized application components.
type BootstrapResult struct {
Cfg *config.Config
LogLevel slog.Level
TikiStore *tikistore.TikiStore
TaskStore store.Store
BoardConfig *model.BoardConfig
HeaderConfig *model.HeaderConfig
LayoutModel *model.LayoutModel
Plugins []plugin.Plugin
PluginConfigs map[string]*model.PluginConfig
PluginDefs map[string]plugin.Plugin
App *tview.Application
Controllers *Controllers
InputRouter *controller.InputRouter
ViewFactory *view.ViewFactory
HeaderWidget *header.HeaderWidget
RootLayout *view.RootLayout
Context context.Context
CancelFunc context.CancelFunc
TikiSkillContent string
DokiSkillContent string
}
// Bootstrap orchestrates the complete application initialization sequence.
// It takes the embedded AI skill content and returns all initialized components.
func Bootstrap(tikiSkillContent, dokiSkillContent string) (*BootstrapResult, error) {
// Phase 1: Pre-flight checks
EnsureGitRepoOrExit()
// Phase 2: Configuration and logging
cfg := LoadConfigOrExit()
logLevel := InitLogging(cfg)
// Phase 3: Project initialization
proceed := EnsureProjectInitialized(tikiSkillContent, dokiSkillContent)
if !proceed {
return nil, nil // User chose not to proceed
}
// Phase 4: Store initialization
tikiStore, taskStore := InitStores()
// Phase 5: Model initialization
boardConfig := InitBoardConfig()
headerConfig, layoutModel := InitHeaderAndLayoutModels()
InitHeaderBaseStats(headerConfig, tikiStore)
// Phase 6: Plugin system
plugins := LoadPlugins()
InitPluginActionRegistry(plugins)
syncHeaderPluginActions(headerConfig)
pluginConfigs, pluginDefs := BuildPluginConfigsAndDefs(plugins)
// Phase 7: Application and controllers
application := app.NewApp()
app.SetupSignalHandler(application)
controllers := BuildControllers(
application,
taskStore,
boardConfig,
plugins,
pluginConfigs,
)
// Phase 8: Input routing
inputRouter := controller.NewInputRouter(
controllers.Nav,
controllers.Board,
controllers.Task,
controllers.Plugins,
taskStore,
)
// Phase 9: View factory and layout
viewFactory := view.NewViewFactory(taskStore, boardConfig)
viewFactory.SetPlugins(pluginConfigs, pluginDefs, controllers.Plugins)
headerWidget := header.NewHeaderWidget(headerConfig)
rootLayout := view.NewRootLayout(headerWidget, headerConfig, layoutModel, viewFactory, taskStore, application)
// Phase 10: View wiring
wireOnViewActivated(rootLayout, application)
// Phase 11: Background tasks
ctx, cancel := context.WithCancel(context.Background())
background.StartBurndownHistoryBuilder(ctx, tikiStore, headerConfig, application)
// Phase 12: Navigation and input wiring
wireNavigation(controllers.Nav, layoutModel, rootLayout)
app.InstallGlobalInputCapture(application, headerConfig, inputRouter, controllers.Nav)
// Phase 13: Initial view
controllers.Nav.PushView(model.BoardViewID, nil)
return &BootstrapResult{
Cfg: cfg,
LogLevel: logLevel,
TikiStore: tikiStore,
TaskStore: taskStore,
BoardConfig: boardConfig,
HeaderConfig: headerConfig,
LayoutModel: layoutModel,
Plugins: plugins,
PluginConfigs: pluginConfigs,
PluginDefs: pluginDefs,
App: application,
Controllers: controllers,
InputRouter: inputRouter,
ViewFactory: viewFactory,
HeaderWidget: headerWidget,
RootLayout: rootLayout,
Context: ctx,
CancelFunc: cancel,
TikiSkillContent: tikiSkillContent,
DokiSkillContent: dokiSkillContent,
}, nil
}
// syncHeaderPluginActions syncs plugin action shortcuts from the controller registry
// into the header model.
func syncHeaderPluginActions(headerConfig *model.HeaderConfig) {
pluginActionsList := convertPluginActions(controller.GetPluginActions())
headerConfig.SetPluginActions(pluginActionsList)
}
// convertPluginActions converts controller.ActionRegistry to []model.HeaderAction
// for HeaderConfig.
func convertPluginActions(registry *controller.ActionRegistry) []model.HeaderAction {
if registry == nil {
return nil
}
actions := registry.GetHeaderActions()
result := make([]model.HeaderAction, len(actions))
for i, a := range actions {
result[i] = model.HeaderAction{
ID: string(a.ID),
Key: a.Key,
Rune: a.Rune,
Label: a.Label,
Modifier: a.Modifier,
ShowInHeader: a.ShowInHeader,
}
}
return result
}
// wireOnViewActivated wires focus setters into views as they become active.
func wireOnViewActivated(rootLayout *view.RootLayout, app *tview.Application) {
rootLayout.SetOnViewActivated(func(v controller.View) {
if titleEditableView, ok := v.(controller.TitleEditableView); ok {
titleEditableView.SetFocusSetter(func(p tview.Primitive) {
app.SetFocus(p)
})
}
if descEditableView, ok := v.(controller.DescriptionEditableView); ok {
descEditableView.SetFocusSetter(func(p tview.Primitive) {
app.SetFocus(p)
})
}
})
}
// wireNavigation wires navigation controller callbacks to keep LayoutModel
// and RootLayout in sync.
func wireNavigation(navController *controller.NavigationController, layoutModel *model.LayoutModel, rootLayout *view.RootLayout) {
navController.SetOnViewChanged(func(viewID model.ViewID, params map[string]interface{}) {
layoutModel.SetContent(viewID, params)
})
navController.SetActiveViewGetter(rootLayout.GetContentView)
}