package ui import ( "fmt" "github.com/charmbracelet/lipgloss" ) var ( titleStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#FFFFFF")). Background(lipgloss.Color("#900C3F")). Padding(0, 1) headerBgStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#FFFFFF")). Background(lipgloss.Color("#1F618D")) localHeaderBgStyle = lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("#FFFFFF")). Background(lipgloss.Color("#B7950B")) selectedRowStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFFFF")). Background(lipgloss.Color("#2980B9")). Bold(true) normalProjectStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#F4D03F")). Bold(true) normalImageStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#D5D8DC")) fullNameStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#00FFFF")). Bold(true) footerKeyStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#000000")). Background(lipgloss.Color("#F4D03F")). Bold(true) footerDescStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FFFFFF")). Background(lipgloss.Color("#2C3E50")) errorBoxStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#FF5F56")). Bold(true) successStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("#2ECC71")). Bold(true) ) func (m Model) View() string { if m.state == StateRegistryDown { return m.viewRegistryDown() } switch m.state { case StateLoading: modeText := "del Registro Docker" if m.mode == ModeLocal { modeText = "Locales de Docker" } return fmt.Sprintf("\n Cargando imágenes %s...\n", modeText) case StateError: return fmt.Sprintf("\n %s\n\n [Presiona 'q' para salir / 'r' o 'l' para cambiar de modo]\n", errorBoxStyle.Render("Error: "+m.err.Error())) case StateMainTable: s := "" modeStr := "REGISTRY" styleHeader := headerBgStyle if m.mode == ModeLocal { modeStr = "LOCAL" styleHeader = localHeaderBgStyle } titleBar := fmt.Sprintf(" REGISTRY-TUI v%s [%s] ", m.version, modeStr) s += styleHeader.Width(70).Render(titleBar) + "\n" s += "┌───────────────────────┬──────────────────────────────────────────┐\n" s += fmt.Sprintf("│ %-21s │ %-40s │\n", "PROYECTO / RUTA", "IMAGEN : VERSION") s += "├───────────────────────┼──────────────────────────────────────────┤\n" if len(m.rows) == 0 { msgEmpty := "No hay imágenes en el registro" if m.mode == ModeLocal { msgEmpty = "No se encontraron imágenes locales" } s += fmt.Sprintf("│ %-21s │ %-40s │\n", "", msgEmpty) s += "└───────────────────────┴──────────────────────────────────────────┘\n" } else { // --- VENTANA DESLIZANTE REDUCIDA A 6 FILAS PARA TERMINALES DE 24 LÍNEAS --- maxVisibleRows := 6 startIdx := 0 endIdx := len(m.rows) if len(m.rows) > maxVisibleRows { startIdx = m.cursor - (maxVisibleRows / 2) if startIdx < 0 { startIdx = 0 } endIdx = startIdx + maxVisibleRows if endIdx > len(m.rows) { endIdx = len(m.rows) startIdx = endIdx - maxVisibleRows } } if startIdx > 0 { s += fmt.Sprintf("│ ... (%d elementos más arriba) │\n", startIdx) s += "├───────────────────────┼──────────────────────────────────────────┤\n" } for i := startIdx; i < endIdx; i++ { row := m.rows[i] displayProj := row.Project if displayProj == "(raíz)" { displayProj = "" } imageVersion := fmt.Sprintf("%s:%s", row.ImageName, row.Tag) isSelected := (m.cursor == i) var lineContent string if isSelected { projFormatted := fmt.Sprintf(" %-21s ", displayProj) imgFormatted := fmt.Sprintf(" %-40s ", imageVersion) renderedProj := selectedRowStyle.Render(projFormatted) renderedImg := selectedRowStyle.Render(imgFormatted) lineContent = fmt.Sprintf("│%s│%s│", renderedProj, renderedImg) } else { projFormatted := normalProjectStyle.Render(fmt.Sprintf("%-21s", displayProj)) imgFormatted := normalImageStyle.Render(fmt.Sprintf("%-40s", imageVersion)) lineContent = fmt.Sprintf("│ %s │ %s │", projFormatted, imgFormatted) } s += lineContent + "\n" if i < endIdx-1 { s += "├───────────────────────┼──────────────────────────────────────────┤\n" } } if endIdx < len(m.rows) { s += "├───────────────────────┼──────────────────────────────────────────┤\n" s += fmt.Sprintf("│ ... (%d elementos más abajo) │\n", len(m.rows)-endIdx) } s += "└───────────────────────┴──────────────────────────────────────────┘\n" } // Ruta completa seleccionada (compacta) if len(m.rows) > 0 { selected := m.rows[m.cursor] fullRef := fmt.Sprintf("%s:%s", selected.RepoName, selected.Tag) s += fmt.Sprintf("Ruta: %s\n", fullNameStyle.Render(fullRef)) } else { s += "\n" } total := len(m.rows) statusInfo := fmt.Sprintf("Total: %d imágenes (%s)", total, modeStr) if m.statusMsg != "" { statusInfo = successStyle.Render("✔ " + m.statusMsg) } s += fmt.Sprintf("%-70s\n", statusInfo) // Barra de botones inferior estilo MC btnR := footerKeyStyle.Render(" R ") + footerDescStyle.Render(" Registry ") btnL := footerKeyStyle.Render(" L ") + footerDescStyle.Render(" Local ") btnT := footerKeyStyle.Render(" t ") + footerDescStyle.Render(" Tagear ") btnD := footerKeyStyle.Render(" d ") + footerDescStyle.Render(" Borrar ") btnP := footerKeyStyle.Render(" p ") + footerDescStyle.Render(" Push ") btnQ := footerKeyStyle.Render(" q ") + footerDescStyle.Render(" Salir ") if m.mode == ModeLocal { s += fmt.Sprintf("%s %s %s %s %s %s", btnR, btnL, btnP, btnT, btnD, btnQ) } else { s += fmt.Sprintf("%s %s %s %s %s", btnR, btnL, btnT, btnD, btnQ) } return s case StateConfirmDelete: selectedRow := m.rows[m.cursor] s := headerBgStyle.Width(50).Render(" Confirmar Borrado ") + "\n\n" s += fmt.Sprintf(" ¿Estás seguro de que deseas borrar la imagen:\n -> %s:%s?\n\n", selectedRow.RepoName, selectedRow.Tag) if m.mode == ModeLocal { s += errorBoxStyle.Render(" ¡Esta acción eliminará la imagen de tu sistema local!\n\n") } else { s += errorBoxStyle.Render(" ¡Esta acción eliminará el manifest permanentemente del registro!\n\n") } s += footerKeyStyle.Render(" y ") + footerDescStyle.Render(" Sí, borrar ") + " " + footerKeyStyle.Render(" n ") + footerDescStyle.Render(" Cancelar ") return s case StateNewTag: selectedRow := m.rows[m.cursor] s := headerBgStyle.Width(50).Render(" Reestructurar / Tagear Imagen ") + "\n\n" s += fmt.Sprintf(" Origen: %s:%s\n\n", selectedRow.RepoName, selectedRow.Tag) s += " Introduce la ruta completa de destino\n (ej: proyecto-web/alpine:v1.0):\n\n" s += " " + m.textInput.View() + "\n\n" s += footerKeyStyle.Render(" Enter ") + footerDescStyle.Render(" Confirmar ") + " " + footerKeyStyle.Render(" Esc ") + footerDescStyle.Render(" Cancelar ") return s } return "" } func (m Model) viewRegistryDown() string { s := titleStyle.Render(" ⚠️ ADVERTENCIA DE CONEXIÓN - REGISTRY-TUI ") + "\n\n" s += fmt.Sprintf(" No se puede conectar al Registry local:\n %s\n\n", m.statusMsg) s += " ─────────────────────────────────────────────────────────\n" s += " [ENTER / L] Cambiar directamente al modo LOCAL\n" s += " [ q ] Salir de la aplicación\n" return lipgloss.NewStyle().Margin(2, 4).Render(s) }