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 }