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>
252 lines
5.6 KiB
Go
252 lines
5.6 KiB
Go
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
|
|
}
|