5d3b56fe8e
Separa aplicaciones ejecutables (docker_tui, pipeline_launcher) de la librería fn_operations. La carpeta apps/ contiene módulos Go independientes, fn_operations/ queda como librería pura de models/store/operations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
202 lines
4.7 KiB
Go
202 lines
4.7 KiB
Go
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
|
|
}
|