package ui import ( "registry-tui/client" "sort" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" ) type ViewState int const ( StateLoading ViewState = iota StateMainTable StateConfirmDelete StateNewTag StateError StateRegistryDown ) type AppMode int const ( ModeRegistry AppMode = iota ModeLocal ) type ImageRow = client.ImageRowData type Model struct { state ViewState mode AppMode client *client.RegistryClient rows []ImageRow localRows []ImageRow registryRows []ImageRow cursor int err error statusMsg string textInput textinput.Model version string } func NewModel(registryURL string, version string) Model { ti := textinput.New() ti.Placeholder = "ej. proyecto-web/alpine:v1.0" ti.CharLimit = 128 ti.Width = 35 return Model{ state: StateLoading, mode: ModeRegistry, client: client.NewRegistryClient(registryURL), textInput: ti, version: version, } } func (m Model) Init() tea.Cmd { return fetchRegistryImagesCmd(m.client) } type imagesMsg struct { rows []ImageRow mode AppMode } type errorMsg error type successMsg string func fetchRegistryImagesCmd(c *client.RegistryClient) tea.Cmd { return func() tea.Msg { repos, err := c.GetCatalog() if err != nil { return errorMsg(err) } var rows []ImageRow for _, repo := range repos { tags, err := c.GetTags(repo) if err != nil { continue } var project, imageName string parts := strings.Split(repo, "/") if len(parts) > 1 { project = parts[0] imageName = strings.Join(parts[1:], "/") } else { project = "" imageName = repo } for _, tag := range tags { rows = append(rows, ImageRow{ Project: project, RepoName: repo, ImageName: imageName, Tag: tag, }) } } sortRows(rows) return imagesMsg{rows: rows, mode: ModeRegistry} } } func fetchLocalImagesCmd(c *client.RegistryClient) tea.Cmd { return func() tea.Msg { rows, err := c.GetLocalImages() if err != nil { return errorMsg(err) } sortRows(rows) return imagesMsg{rows: rows, mode: ModeLocal} } } func sortRows(rows []ImageRow) { sort.Slice(rows, func(i, j int) bool { if rows[i].Project == rows[j].Project { return rows[i].RepoName < rows[j].RepoName } return rows[i].Project < rows[j].Project }) } func deleteTagCmd(c *client.RegistryClient, repo, tag string) tea.Cmd { return func() tea.Msg { err := c.DeleteManifest(repo, tag) if err != nil { return errorMsg(err) } return successMsg("Tag borrado con éxito del registro") } } func deleteLocalImageCmd(c *client.RegistryClient, repo, tag string) tea.Cmd { return func() tea.Msg { err := c.DeleteLocalImage(repo, tag) if err != nil { return errorMsg(err) } return successMsg("Imagen local borrada con éxito") } } func copyImageCmd(c *client.RegistryClient, sourceRepo, sourceTag, destRepo, destTag string) tea.Cmd { return func() tea.Msg { err := c.CopyManifest(sourceRepo, sourceTag, destRepo, destTag) if err != nil { return errorMsg(err) } return successMsg("Imagen reestructurada con éxito en el registro") } } func tagLocalImageCmd(c *client.RegistryClient, sourceRepo, sourceTag, destRepo, destTag string) tea.Cmd { return func() tea.Msg { err := c.TagLocalImage(sourceRepo, sourceTag, destRepo, destTag) if err != nil { return errorMsg(err) } return successMsg("Imagen local etiquetada con éxito") } } func pushImageCmd(c *client.RegistryClient, repo, tag string) tea.Cmd { return func() tea.Msg { err := c.PushLocalImage(repo, tag) if err != nil { return errorMsg(err) } return successMsg("Imagen subida (Push) al registro con éxito") } }