edfff680b3
TUI fullscreen con dos tabs: Pipelines (lista filtrable del registry, lanzamiento como subproceso, registro automático en operations.db) y History (historial de ejecuciones con status, duración y detalles). Incluye launcher.sh en la raíz para ejecución rápida. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
182 lines
3.8 KiB
Go
182 lines
3.8 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
ops "fn-registry/fn_operations"
|
|
"fn-registry/registry"
|
|
"pipeline-launcher/config"
|
|
"pipeline-launcher/views"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/lucasdataproyects/devfactory/tui"
|
|
)
|
|
|
|
// View identifies which tab is active.
|
|
type View int
|
|
|
|
const (
|
|
ViewPipelines View = iota
|
|
ViewHistory
|
|
)
|
|
|
|
var tabNames = []string{"Pipelines", "History"}
|
|
|
|
// Model is the top-level TUI model with two tabs.
|
|
type Model struct {
|
|
tui.BaseModel
|
|
activeTab int
|
|
pipelines views.PipelinesModel
|
|
history views.HistoryModel
|
|
ready bool
|
|
registryDB *registry.DB
|
|
opsDB *ops.DB
|
|
}
|
|
|
|
// New creates the Model, opening both databases.
|
|
func New(cfg config.Config) (Model, error) {
|
|
regDB, err := registry.Open(cfg.RegistryDB)
|
|
if err != nil {
|
|
return Model{}, fmt.Errorf("opening registry: %w", err)
|
|
}
|
|
|
|
opsDB, err := ops.Open(cfg.OperationsDB)
|
|
if err != nil {
|
|
regDB.Close()
|
|
return Model{}, fmt.Errorf("opening operations: %w", err)
|
|
}
|
|
|
|
// Build pipeline name map for history view
|
|
fns, _ := regDB.SearchFunctions("", registry.KindPipeline, "", "", "")
|
|
names := make(map[string]string, len(fns))
|
|
for _, f := range fns {
|
|
names[f.ID] = f.Name
|
|
}
|
|
|
|
styles := tui.DarkStyles()
|
|
|
|
return Model{
|
|
BaseModel: tui.NewBaseModel().WithStyles(styles),
|
|
pipelines: views.NewPipelinesModel(styles, regDB, opsDB, cfg.RegistryRoot),
|
|
history: views.NewHistoryModel(styles, opsDB, names),
|
|
registryDB: regDB,
|
|
opsDB: opsDB,
|
|
}, nil
|
|
}
|
|
|
|
// Close closes both database connections.
|
|
func (m Model) Close() {
|
|
if m.registryDB != nil {
|
|
m.registryDB.Close()
|
|
}
|
|
if m.opsDB != nil {
|
|
m.opsDB.Close()
|
|
}
|
|
}
|
|
|
|
func (m Model) Init() tea.Cmd {
|
|
return m.pipelines.Init()
|
|
}
|
|
|
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case views.KeyQuit:
|
|
return m, tea.Quit
|
|
case "q":
|
|
updated, atBase := m.handleBack()
|
|
if atBase {
|
|
return updated, tea.Quit
|
|
}
|
|
return updated, nil
|
|
case views.KeyEsc, views.KeyBack:
|
|
updated, atBase := m.handleBack()
|
|
if atBase {
|
|
return updated, nil
|
|
}
|
|
return updated, nil
|
|
case views.KeyTab:
|
|
m.activeTab = (m.activeTab + 1) % len(tabNames)
|
|
return m, m.initActiveView()
|
|
case "shift+tab":
|
|
m.activeTab = (m.activeTab - 1 + len(tabNames)) % len(tabNames)
|
|
return m, m.initActiveView()
|
|
}
|
|
case tea.WindowSizeMsg:
|
|
m.HandleWindowSize(msg)
|
|
m.ready = true
|
|
}
|
|
|
|
var cmd tea.Cmd
|
|
switch View(m.activeTab) {
|
|
case ViewPipelines:
|
|
m.pipelines, cmd = m.pipelines.Update(msg)
|
|
case ViewHistory:
|
|
m.history, cmd = m.history.Update(msg)
|
|
}
|
|
return m, cmd
|
|
}
|
|
|
|
func (m Model) View() string {
|
|
if !m.ready {
|
|
return "Loading..."
|
|
}
|
|
|
|
tabs := m.renderTabs()
|
|
|
|
var content string
|
|
switch View(m.activeTab) {
|
|
case ViewPipelines:
|
|
content = m.pipelines.View()
|
|
case ViewHistory:
|
|
content = m.history.View()
|
|
}
|
|
|
|
status := m.Styles.StatusBar.Render(" Tab: switch view │ Ctrl+C: quit │ Enter: action │ r: refresh")
|
|
|
|
return lipgloss.JoinVertical(lipgloss.Left,
|
|
tabs,
|
|
"",
|
|
content,
|
|
"",
|
|
status,
|
|
)
|
|
}
|
|
|
|
func (m Model) renderTabs() string {
|
|
var tabs []string
|
|
for i, name := range tabNames {
|
|
if i == m.activeTab {
|
|
tabs = append(tabs, m.Styles.Selected.Render(" "+name+" "))
|
|
} else {
|
|
tabs = append(tabs, m.Styles.Muted.Render(" "+name+" "))
|
|
}
|
|
}
|
|
row := lipgloss.JoinHorizontal(lipgloss.Top, tabs...)
|
|
return m.Styles.Header.Render("Pipeline Launcher") + " " + row
|
|
}
|
|
|
|
func (m Model) handleBack() (Model, bool) {
|
|
switch View(m.activeTab) {
|
|
case ViewPipelines:
|
|
atBase := m.pipelines.HandleBack()
|
|
return m, atBase
|
|
case ViewHistory:
|
|
atBase := m.history.HandleBack()
|
|
return m, atBase
|
|
}
|
|
return m, true
|
|
}
|
|
|
|
func (m Model) initActiveView() tea.Cmd {
|
|
switch View(m.activeTab) {
|
|
case ViewPipelines:
|
|
return m.pipelines.Init()
|
|
case ViewHistory:
|
|
return m.history.Init()
|
|
}
|
|
return nil
|
|
}
|