package views import ( "fmt" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/lucasdataproyects/devfactory/tui" ) type composeState int const ( composeLoading composeState = iota composeList composeAction composeLogs ) type composeLoadedMsg []ComposeService type composeActionMsg struct{ output string; err error } type composeLogsMsg struct{ output string; err error } type ComposeModel struct { state composeState list tui.ListModel spinner tui.SpinnerModel styles tui.Styles services []ComposeService output string scrollOff int err error } func NewComposeModel(styles tui.Styles) ComposeModel { return ComposeModel{ state: composeLoading, list: tui.NewList(nil), spinner: tui.NewSpinner("Loading compose services..."), styles: styles, } } func (m ComposeModel) Init() tea.Cmd { return tea.Batch(m.spinner.Init(), loadCompose) } func loadCompose() tea.Msg { services, err := ComposePS() if err != nil { return composeLoadedMsg(nil) } return composeLoadedMsg(services) } func (m ComposeModel) Update(msg tea.Msg) (ComposeModel, tea.Cmd) { switch msg := msg.(type) { case composeLoadedMsg: m.services = []ComposeService(msg) items := make([]tui.ListItem, 0, len(m.services)+2) // Add action items at the top items = append(items, tui.ListItem{Title: "▶ Compose Up", Description: "docker compose up -d", Value: "up"}, tui.ListItem{Title: "■ Compose Down", Description: "docker compose down", Value: "down"}, ) for _, s := range m.services { stateIcon := "●" if s.State == "running" { stateIcon = "▶" } items = append(items, tui.ListItem{ Title: fmt.Sprintf("%s %s", stateIcon, s.Name), Description: fmt.Sprintf("Service: %s — %s", s.Service, s.Status), Value: s, }) } m.list.SetItems(items) m.state = composeList return m, nil case composeActionMsg: m.output = msg.output if msg.err != nil { m.output = fmt.Sprintf("Error: %v", msg.err) } m.state = composeList return m, loadCompose case composeLogsMsg: m.output = msg.output if msg.err != nil { m.output = fmt.Sprintf("Error: %v", msg.err) } m.state = composeLogs m.scrollOff = 0 return m, nil case tea.KeyMsg: switch m.state { case composeList: switch msg.String() { case "r": m.state = composeLoading return m, tea.Batch(m.spinner.Init(), loadCompose) case "l": m.state = composeAction return m, func() tea.Msg { output, err := ComposeLogs(100) return composeLogsMsg{output: output, err: err} } case "enter": if item := m.list.SelectedItem(); item != nil { switch v := item.Value.(type) { case string: m.state = composeAction if v == "up" { return m, func() tea.Msg { output, err := ComposeUp() return composeActionMsg{output: output, err: err} } } return m, func() tea.Msg { output, err := ComposeDown() return composeActionMsg{output: output, err: err} } } } } case composeLogs: switch msg.String() { case "j", "down": m.scrollOff++ case "k", "up": if m.scrollOff > 0 { m.scrollOff-- } } return m, nil } } var cmd tea.Cmd switch m.state { case composeLoading, composeAction: var model tea.Model model, cmd = m.spinner.Update(msg) m.spinner = model.(tui.SpinnerModel) case composeList: var model tea.Model model, cmd = m.list.Update(msg) m.list = model.(tui.ListModel) } return m, cmd } // HandleBack retrocede un nivel. Retorna true si ya estaba en estado base. func (m *ComposeModel) HandleBack() bool { switch m.state { case composeLogs: m.state = composeList return false default: return true } } func (m ComposeModel) View() string { switch m.state { case composeLoading, composeAction: return m.spinner.View() case composeList: if len(m.services) == 0 { help := m.styles.Muted.Render(" No compose services. Use Enter on 'Compose Up' or press 'r' to refresh.") return m.list.View() + "\n" + help } help := m.styles.Muted.Render(" Enter: up/down │ l: logs │ r: refresh") return m.list.View() + "\n" + help case composeLogs: return m.renderLogs() } return "" } func (m ComposeModel) renderLogs() string { lines := strings.Split(m.output, "\n") if len(lines) == 0 { lines = []string{"(empty)"} } 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("Compose Logs") content := strings.Join(visible, "\n") help := m.styles.Muted.Render(" j/k: scroll │ Esc: back") return header + "\n" + content + "\n" + help }