package tui import ( "fmt" "strings" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" ) const ( header = "App do Dono - Instalador Cliente" anyKeyOutMsg = "qualquer tecla: sair" retryMsg = "r: tentar novamente • q: sair" fixMsg = "qualquer tecla: voltar e corrigir" formBase = "tab: próximo campo • enter: confirmar" ipBase = "↑/↓: navegar • enter: selecionar" reviewBase = "enter: confirmar e instalar" ) // helpFor builds the footer for an input step, appending "esc: voltar" when back // navigation is available and always ending with "ctrl+c: sair". func (m Model) helpFor(base string) string { parts := []string{} if base != "" { parts = append(parts, base) } if m.canGoBack() { parts = append(parts, "esc: voltar") } parts = append(parts, "ctrl+c: sair") return strings.Join(parts, " • ") } func (m Model) View() tea.View { pad := strings.Repeat(" ", padding) var body string helpMsg := m.helpFor("") switch m.currentStep { // Docker stuff case StepCheckDocker: body = m.viewCheckDocker() case StepDockerInstall: body = m.viewDockerInstall() helpMsg = anyKeyOutMsg case StepDockerLogin: body = m.loginForm.View() helpMsg = m.helpFor(formBase) case StepDownloadImage: body = m.viewDownloadImage() if m.downloadDone && m.downloadError != nil { helpMsg = retryMsg } // IP Stuff case StepIPQuestion: body = m.viewIPQuestion() helpMsg = m.helpFor(ipBase) case StepWireguardConfig: body = m.wireguardForm.View() helpMsg = m.helpFor(formBase) case StepGenerateWireguardFile: body = m.viewGenerateFile() if m.finishedFile && m.configFileError != nil { helpMsg = fixMsg } case StepDownloadWireguard: body = m.viewDownloadImage() if m.downloadDone && m.downloadError != nil { helpMsg = retryMsg } case StepRunWireguard: body = m.viewDockerRun() if m.finishedDockerRun && m.dockerRunError != nil { helpMsg = retryMsg } // App Config Stuff case StepAppConfig: body = m.appForm.View() helpMsg = m.helpFor(formBase) case StepServerConfig: body = m.serverForm.View() helpMsg = m.helpFor(formBase) case StepDatabaseConfig: body = m.dbForm.View() helpMsg = m.helpFor(formBase) case StepCertConfig: body = m.certForm.View() helpMsg = m.helpFor(formBase) // Review case StepReview: body = m.viewReview() helpMsg = m.helpFor(reviewBase) // Finalize case StepGenerateFile: body = m.viewGenerateFile() if m.finishedFile && m.configFileError != nil { helpMsg = fixMsg } case StepRunDocker: body = m.viewDockerRun() if m.finishedDockerRun && m.dockerRunError != nil { helpMsg = retryMsg } case StepDone: body = m.viewDoneMessage() helpMsg = anyKeyOutMsg } help := HelpStyle.Render(helpMsg) title := TitleStyle.Render(header) + HelpStyle.Render(" v"+Version) v := tea.NewView(fmt.Sprintf("\n%s%s\n\n%s\n\n%s%s\n", pad, title, body, pad, help, )) v.AltScreen = true return v } func (m Model) viewCheckDocker() string { pad := strings.Repeat(" ", padding) barWidth := 40 filled := int(m.checkProgress * float64(barWidth)) empty := barWidth - filled bar := ProgressFillStyle.Render(strings.Repeat("█", filled)) + ProgressEmptyStyle.Render(strings.Repeat("░", empty)) percent := fmt.Sprintf(" %d%%", int(m.checkProgress*100)) var sb strings.Builder sb.WriteString(pad + "Avaliando instalação do Docker...\n\n") sb.WriteString(pad + bar + HelpStyle.Render(percent)) if m.checkDockerDone && m.checkProgress == 1 { found := "" if m.dockerInstalled { found = "encontrado" } else { found = "não encontrado" } sb.WriteString("\n\n" + pad + fmt.Sprintf("Docker %s na máquina. Pressione qualquer tecla para seguir.", found)) } return sb.String() } func (m Model) viewDockerInstall() string { pad := strings.Repeat(" ", padding) var sb strings.Builder sb.WriteString(pad + "Nenhuma versão do Docker encontrada no sistema.\n") sb.WriteString(pad + "Instale o Docker manualmente e execute este instalador novamente para concluir a configuração.") return sb.String() } func (m Model) viewDownloadImage() string { pad := strings.Repeat(" ", padding) var sb strings.Builder if !m.downloadDone { sb.WriteString(pad + fmt.Sprintf("%s Baixando imagem do container...\n\n", m.spinner.View())) return sb.String() } if m.downloadError == nil { sb.WriteString(pad + "Instalação concluída com êxito.\n\n") sb.WriteString(pad + "Pressione qualquer tecla para continuar.") } else { sb.WriteString(pad + "Instalação finalizada com erros.\n\n") dualPad := pad + pad errText := dualPad + m.downloadMessage + "\n" + dualPad + m.downloadError.Error() sb.WriteString(ErrorStyle.Width(m.width - (padding * 2)).Align(lipgloss.Left).Render(errText)) sb.WriteString("\n\n" + pad + "Pressione 'r' para tentar novamente ou 'q' para sair.") } return sb.String() } func (m Model) viewIPQuestion() string { pad := strings.Repeat(" ", padding) var options = []string{"Sim", "Não"} var sb strings.Builder sb.WriteString(pad + "Existe IP público disponível para a máquina?\n") for i, opt := range options { cursor := " " text := "" isSelected := m.cursor == i if isSelected { cursor = CursorStyle.Render("> ") text = CursorStyle.Render(opt) } else { text = opt } sb.WriteString("\n" + pad + cursor + text) } return sb.String() } func (m Model) viewReview() string { pad := strings.Repeat(" ", padding) cv := m.configValues var sb strings.Builder sb.WriteString(pad + "Revise as configurações antes de instalar:\n") section := func(title string, rows [][2]string) { sb.WriteString("\n" + pad + TitleStyle.Render(title) + "\n") for _, r := range rows { sb.WriteString(pad + HelpStyle.Render(r[0]+": ") + r[1] + "\n") } } if len(cv.Wireguard) > 0 { section("vproxy", [][2]string{ {"IP Virtual", cv.Wireguard["vip"]}, {"Proxy EDPS", cv.Wireguard["proxy_edps"]}, {"MTU", cv.Wireguard["mtu"]}, {"Protocolo", cv.Wireguard["proto"]}, }) } section("Aplicação", [][2]string{ {"Servidor Central", cv.Application["central_server_url"]}, {"Token de Inscrição", cv.Application["enrollment_token"]}, }) section("Servidor", [][2]string{ {"Porta (host)", cv.Server["port"]}, {"Timeout", cv.Server["timeout"]}, {"Ambiente", cv.Server["environment"]}, {"Modo Compatibilidade", cv.Server["seccomp_unconfined"]}, }) section("Banco de Dados", [][2]string{ {"Tipo", cv.Database["database_type"]}, {"URL", cv.Database["database_url"]}, {"Conexões (máx/mín)", cv.Database["max_conns"] + "/" + cv.Database["min_conns"]}, }) section("Certificado", [][2]string{ {"Diretório", cv.Cert["cert_dir_path"]}, }) return sb.String() } func (m Model) viewGenerateFile() string { pad := strings.Repeat(" ", padding) var sb strings.Builder if !m.finishedFile { sb.WriteString(pad + m.spinner.View() + "Gerando arquivo. Aguarde...") } else { sb.WriteString(pad + "Geração do arquivo finalizada.\n\n") if m.configFileError == nil { sb.WriteString(pad + "Arquivo gerado com sucesso. Pressione qualquer tecla para seguir.") } else { sb.WriteString(pad + "Ocorreu um erro ao gerar o arquivo.\n\n") dualPad := pad + pad errText := dualPad + m.configFileError.Error() sb.WriteString(ErrorStyle.Width(m.width - (padding * 2)).Align(lipgloss.Left).Render(errText)) sb.WriteString("\n\n" + pad + "Pressione qualquer tecla para voltar e corrigir os dados.") } } return sb.String() } func (m Model) viewDockerRun() string { pad := strings.Repeat(" ", padding) var sb strings.Builder if !m.finishedDockerRun { sb.WriteString(pad + m.spinner.View() + "Tentando iniciar o container Docker. Aguarde...") } else { sb.WriteString(pad + "Inicialização finalizada.\n\n") if m.dockerRunError == nil { sb.WriteString(pad + "Container Docker iniciado com sucesso. Pressione qualquer tecla para seguir.") } else { sb.WriteString(pad + "Ocorreu um erro ao iniciar o container.\n\n") dualPad := pad + pad errText := dualPad + m.dockerRunError.Error() sb.WriteString(ErrorStyle.Width(m.width - (padding * 2)).Align(lipgloss.Left).Render(errText)) sb.WriteString("\n\n" + pad + "Pressione 'r' para tentar novamente ou 'q' para sair.") } } return sb.String() } func (m Model) viewDoneMessage() string { pad := strings.Repeat(" ", padding) var sb strings.Builder sb.WriteString(pad + "Instalação realizada com sucesso!") return sb.String() }