package views import ( "fmt" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/lucasdataproyects/devfactory/tui" ) type containersState int const ( containersLoading containersState = iota containersList containersAction containersLogs ) type containersLoadedMsg []DockerContainer type containersActionMsg struct{ output string; err error } type containersLogsMsg struct{ output string; err error } type ContainersModel struct { state containersState list tui.FilteredListModel spinner tui.SpinnerModel styles tui.Styles containers []DockerContainer output string scrollOff int err error } func NewContainersModel(styles tui.Styles) ContainersModel { return ContainersModel{ state: containersLoading, list: tui.NewFilteredList(nil, "Filter containers..."), spinner: tui.NewSpinner("Loading containers..."), styles: styles, } } func (m ContainersModel) Init() tea.Cmd { return tea.Batch( m.spinner.Init(), loadContainers, ) } func loadContainers() tea.Msg { containers, err := ListContainers() if err != nil { return containersLoadedMsg(nil) } return containersLoadedMsg(containers) } func (m ContainersModel) Update(msg tea.Msg) (ContainersModel, tea.Cmd) { switch msg := msg.(type) { case containersLoadedMsg: m.containers = []DockerContainer(msg) items := make([]tui.ListItem, len(m.containers)) for i, c := range m.containers { stateIcon := "●" if c.State == "running" { stateIcon = "▶" } else if c.State == "exited" { stateIcon = "■" } items[i] = tui.ListItem{ Title: fmt.Sprintf("%s %s", stateIcon, c.Names), Description: fmt.Sprintf("%s — %s", c.Image, c.Status), Value: c, } } m.list.SetItems(items) m.state = containersList return m, nil case containersActionMsg: m.output = msg.output if msg.err != nil { m.output = fmt.Sprintf("Error: %v", msg.err) } m.state = containersList // Refresh after action return m, loadContainers case containersLogsMsg: m.output = msg.output if msg.err != nil { m.output = fmt.Sprintf("Error: %v", msg.err) } m.state = containersLogs m.scrollOff = 0 return m, nil case tea.KeyMsg: switch m.state { case containersList: switch msg.String() { case "r": m.state = containersLoading return m, tea.Batch(m.spinner.Init(), loadContainers) case "enter": if item := m.list.SelectedItem(); item != nil { c := item.Value.(DockerContainer) if c.State == "running" { return m, stopContainerCmd(c.ID) } return m, startContainerCmd(c.ID) } case "l": if item := m.list.SelectedItem(); item != nil { c := item.Value.(DockerContainer) m.state = containersAction return m, logsContainerCmd(c.ID) } case "x": if item := m.list.SelectedItem(); item != nil { c := item.Value.(DockerContainer) return m, restartContainerCmd(c.ID) } } case containersLogs: 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 containersLoading: var spinnerModel tea.Model spinnerModel, cmd = m.spinner.Update(msg) m.spinner = spinnerModel.(tui.SpinnerModel) case containersList: var listModel tea.Model listModel, cmd = m.list.Update(msg) m.list = listModel.(tui.FilteredListModel) } return m, cmd } // HandleBack retrocede un nivel. Retorna true si ya estaba en estado base (el caller debe salir). func (m *ContainersModel) HandleBack() bool { switch m.state { case containersLogs: m.state = containersList return false default: return true } } func (m ContainersModel) View() string { switch m.state { case containersLoading: return m.spinner.View() case containersList: if len(m.containers) == 0 { return m.styles.Muted.Render("No containers found. Press 'r' to refresh.") } help := m.styles.Muted.Render(" Enter: start/stop │ l: logs │ x: restart │ r: refresh │ /: filter") return m.list.View() + "\n" + help case containersAction: return m.spinner.View() case containersLogs: return m.renderOutput() } return "" } func (m ContainersModel) 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("Container Logs") content := lipgloss.JoinVertical(lipgloss.Left, visible...) help := m.styles.Muted.Render(" j/k: scroll │ Esc: back") return header + "\n" + content + "\n" + help } func startContainerCmd(id string) tea.Cmd { return func() tea.Msg { err := StartContainer(id) return containersActionMsg{output: "Started " + id, err: err} } } func stopContainerCmd(id string) tea.Cmd { return func() tea.Msg { err := StopContainer(id) return containersActionMsg{output: "Stopped " + id, err: err} } } func restartContainerCmd(id string) tea.Cmd { return func() tea.Msg { err := RestartContainer(id) return containersActionMsg{output: "Restarted " + id, err: err} } } func logsContainerCmd(id string) tea.Cmd { return func() tea.Msg { output, err := ContainerLogs(id, 100) return containersLogsMsg{output: output, err: err} } } 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 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 }