package views import ( "fmt" "strings" ops "fn-registry/fn_operations" "fn-registry/registry" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/lucasdataproyects/devfactory/tui" ) type pipelinesState int const ( pipelinesLoading pipelinesState = iota pipelinesList pipelinesRunning pipelinesOutput ) type pipelinesLoadedMsg []registry.Function type pipelineFinishedMsg RunResult // PipelinesModel lists and launches pipelines. type PipelinesModel struct { state pipelinesState list tui.FilteredListModel spinner tui.SpinnerModel styles tui.Styles pipelines []registry.Function output string lastResult *RunResult scrollOff int err error registryDB *registry.DB opsDB *ops.DB registryRoot string } // NewPipelinesModel creates a new pipelines view. func NewPipelinesModel(styles tui.Styles, regDB *registry.DB, opsDB *ops.DB, root string) PipelinesModel { return PipelinesModel{ state: pipelinesLoading, list: tui.NewFilteredList(nil, "Filter pipelines..."), spinner: tui.NewSpinner("Loading pipelines..."), styles: styles, registryDB: regDB, opsDB: opsDB, registryRoot: root, } } func (m PipelinesModel) Init() tea.Cmd { return tea.Batch( m.spinner.Init(), m.loadPipelines(), ) } func (m PipelinesModel) loadPipelines() tea.Cmd { return func() tea.Msg { fns, err := m.registryDB.SearchFunctions("", registry.KindPipeline, "", "", "") if err != nil { return pipelinesLoadedMsg(nil) } return pipelinesLoadedMsg(fns) } } func (m PipelinesModel) Update(msg tea.Msg) (PipelinesModel, tea.Cmd) { switch msg := msg.(type) { case pipelinesLoadedMsg: m.pipelines = []registry.Function(msg) items := make([]tui.ListItem, len(m.pipelines)) for i, p := range m.pipelines { items[i] = tui.ListItem{ Title: p.Name, Description: fmt.Sprintf("%s — %s", p.Domain, truncate(p.Description, 60)), Value: p, } } m.list.SetItems(items) m.state = pipelinesList return m, nil case pipelineFinishedMsg: result := RunResult(msg) m.lastResult = &result // Build output var sb strings.Builder if result.Status == ops.ExecSuccess { sb.WriteString("[OK] ") } else { sb.WriteString("[FAIL] ") } sb.WriteString(fmt.Sprintf("Pipeline: %s\n", result.PipelineID)) sb.WriteString(fmt.Sprintf("Execution: %s\n", result.ExecID)) sb.WriteString(fmt.Sprintf("Duration: %dms\n", result.DurationMs)) sb.WriteString("\n--- stdout ---\n") if result.Stdout != "" { sb.WriteString(result.Stdout) } else { sb.WriteString("(empty)") } if result.Stderr != "" { sb.WriteString("\n--- stderr ---\n") sb.WriteString(result.Stderr) } if result.Err != nil { sb.WriteString(fmt.Sprintf("\n--- error ---\n%v", result.Err)) } m.output = sb.String() m.state = pipelinesOutput m.scrollOff = 0 return m, nil case tea.KeyMsg: switch m.state { case pipelinesList: switch msg.String() { case "r": m.state = pipelinesLoading m.spinner = tui.NewSpinner("Loading pipelines...") return m, tea.Batch(m.spinner.Init(), m.loadPipelines()) case "enter": if item := m.list.SelectedItem(); item != nil { fn := item.Value.(registry.Function) m.state = pipelinesRunning m.spinner = tui.NewSpinner(fmt.Sprintf("Running %s...", fn.Name)) return m, tea.Batch(m.spinner.Init(), m.runPipelineCmd(&fn)) } } case pipelinesOutput: switch msg.String() { case "j", "down": m.scrollOff++ case "k", "up": if m.scrollOff > 0 { m.scrollOff-- } } return m, nil } } // Delegate to sub-components var cmd tea.Cmd switch m.state { case pipelinesLoading, pipelinesRunning: var spinnerModel tea.Model spinnerModel, cmd = m.spinner.Update(msg) m.spinner = spinnerModel.(tui.SpinnerModel) case pipelinesList: var listModel tea.Model listModel, cmd = m.list.Update(msg) m.list = listModel.(tui.FilteredListModel) } return m, cmd } func (m PipelinesModel) runPipelineCmd(fn *registry.Function) tea.Cmd { regRoot := m.registryRoot opsDB := m.opsDB fnCopy := *fn return func() tea.Msg { result := RunPipeline(&fnCopy, regRoot, opsDB) return pipelineFinishedMsg(result) } } // HandleBack retrocede un nivel. Retorna true si ya en estado base. func (m *PipelinesModel) HandleBack() bool { switch m.state { case pipelinesOutput: m.state = pipelinesList return false default: return true } } func (m PipelinesModel) View() string { switch m.state { case pipelinesLoading: return m.spinner.View() case pipelinesList: if len(m.pipelines) == 0 { return m.styles.Muted.Render("No pipelines found. Press 'r' to refresh.") } help := m.styles.Muted.Render(" Enter: launch │ r: refresh │ /: filter") return m.list.View() + "\n" + help case pipelinesRunning: return m.spinner.View() case pipelinesOutput: return m.renderOutput() } return "" } func (m PipelinesModel) renderOutput() string { lines := splitLines(m.output) maxLines := 20 if m.scrollOff >= len(lines) { m.scrollOff = max(0, len(lines)-1) } end := min(m.scrollOff+maxLines, len(lines)) visible := lines[m.scrollOff:end] header := m.styles.Header.Render("Pipeline Output") content := lipgloss.JoinVertical(lipgloss.Left, visible...) help := m.styles.Muted.Render(" j/k: scroll │ Esc: back") return header + "\n" + content + "\n" + help } func splitLines(s string) []string { if s == "" { return []string{"(empty)"} } lines := strings.Split(s, "\n") if len(lines) == 0 { return []string{"(empty)"} } return lines } func truncate(s string, n int) string { if len(s) <= n { return s } return s[:n-3] + "..." } func max(a, b int) int { if a > b { return a } return b } func min(a, b int) int { if a < b { return a } return b }