diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab8b69c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.toml \ No newline at end of file diff --git a/internal/docker/docker.go b/internal/docker/docker.go deleted file mode 100644 index 0cec139..0000000 --- a/internal/docker/docker.go +++ /dev/null @@ -1,27 +0,0 @@ -package tui - -import ( - "fmt" - "os/exec" -) - -func RunContainer(image string, name string, port int) error { - - cmd := exec.Command( - "docker", "run", - "-d", - "--name", name, - "-p", fmt.Sprintf("%d:%d", port, port), - image, - ) - - return cmd.Run() -} - -func PullImage(image string) (string, error) { - - cmd := exec.Command("docker", "pull", image) - - out, err := cmd.CombinedOutput() - return string(out), err -} diff --git a/internal/tui/cmds.go b/internal/tui/cmds.go index f52aa00..a3f216c 100644 --- a/internal/tui/cmds.go +++ b/internal/tui/cmds.go @@ -1,6 +1,7 @@ package tui import ( + "fmt" "os/exec" "time" @@ -14,11 +15,18 @@ type DockerCheckedMsg struct{ Installed bool } type DockerInstalledMsg struct{ Err error } type TickMsg struct{} -// Image downloading +// Image downloading and checking +type ImageCheckMsg struct { + Exists bool + Err error +} type ImageDownloadFinishedMsg struct { - Err error + Message string + Err error } type DownloadTickMsg struct{} +type ConfigFileMsg struct{ Err error } +type DockerRunMsg struct{ Err error } // --- Commands --- @@ -29,18 +37,61 @@ func CheckDockerCmd() tea.Cmd { } } -func tickCmd() tea.Cmd { +func TickCmd() tea.Cmd { return tea.Tick(100*time.Millisecond, func(t time.Time) tea.Msg { return TickMsg{} }) } -func downloadImageCmd() tea.Cmd { +func DownloadImageCmd(username, password string) tea.Cmd { return func() tea.Msg { + url := imageName + + loginOut, err := exec.Command( + "docker", "login", url, + "-u", username, + "-p", password, + ).CombinedOutput() + + if err != nil { + return ImageDownloadFinishedMsg{ + Message: string(loginOut), + Err: fmt.Errorf("falha no login: %w", err), + } + } - err := docker.PullImage("hub.davinti.com.br:443/app-dono/app-cliente:latest") + message, err := PullImage(url) + return ImageDownloadFinishedMsg{Message: message, Err: err} + } +} + +func CheckImageCmd(image string) tea.Cmd { + return func() tea.Msg { + cmd := exec.Command("docker", "image", "inspect", image) + err := cmd.Run() + + return ImageCheckMsg{ + Exists: err == nil, + Err: err, + } + } +} + +func GenerateConfigFile(cv ConfigValues, path string) tea.Cmd { + return func() tea.Msg { + err := WriteConfigFile(cv, path) + + return ConfigFileMsg{ + Err: err, + } + } +} + +func RunAppContainer(image, name, filePath, destinationPath string, cv ConfigValues) tea.Cmd { + return func() tea.Msg { + err := RunAppClienteContainer(image, name, filePath, destinationPath, cv) - return ImageDownloadFinishedMsg{ + return DockerRunMsg{ Err: err, } } diff --git a/internal/tui/config.go b/internal/tui/config.go new file mode 100644 index 0000000..66e0a92 --- /dev/null +++ b/internal/tui/config.go @@ -0,0 +1,75 @@ +package tui + +import ( + "fmt" + "os" + "strconv" + "strings" +) + +func GenerateConfigTOML(cv ConfigValues) (string, error) { + var sb strings.Builder + + // [server] + sb.WriteString("# Server Configuration\n") + sb.WriteString("[server]\n") + sb.WriteString(fmt.Sprintf("port = %s\n", cv.Server["port"])) + sb.WriteString(fmt.Sprintf("timeout_seconds = %s\n", cv.Server["timeout"])) + sb.WriteString(fmt.Sprintf("environment = %q\n", cv.Server["environment"])) + sb.WriteString("\n") + + // [database] + sb.WriteString("# Database Configuration\n") + sb.WriteString("[database]\n") + sb.WriteString(fmt.Sprintf("type = %q\n", cv.Database["database_type"])) + sb.WriteString(fmt.Sprintf("url = %q\n", cv.Database["database_url"])) + sb.WriteString(fmt.Sprintf("max_conns = %s\n", cv.Database["max_conns"])) + sb.WriteString(fmt.Sprintf("min_conns = %s\n", cv.Database["min_conns"])) + sb.WriteString("\n") + + // [certificate] + sb.WriteString("# Certificate Options\n") + sb.WriteString("[certificate]\n") + sb.WriteString(fmt.Sprintf("cert_path = %q\n", "/app/certs/"+cv.Cert["cert_name"])) + sb.WriteString(fmt.Sprintf("key_path = %q\n", "/app/certs/"+cv.Cert["key_name"])) + sb.WriteString(fmt.Sprintf("ca_path = %q\n", "/app/certs/"+cv.Cert["ca_name"])) + sb.WriteString(fmt.Sprintf("server_name = %q\n", cv.Cert["server_name"])) + sb.WriteString("\n") + + // [application] — hardcoded / pre-defined + sb.WriteString("# Pre-defined options\n") + sb.WriteString("[application]\n") + sb.WriteString("erp = \"TOTVS\"\n") + sb.WriteString("central_server_url = \"https://warden:8080\"\n") + sb.WriteString("api_key = \"super secreto\"\n") + sb.WriteString("\n") + + // [log] — hardcoded defaults + sb.WriteString("[log]\n") + sb.WriteString("level = \"debug\"\n") + sb.WriteString("format = \"text\" # Options: \"json\" or \"text\"\n") + + return sb.String(), nil +} + +func WriteConfigFile(cv ConfigValues, path string) error { + // Validate numeric fields before writing + numericFields := map[string]string{ + "port": cv.Server["port"], + "timeout": cv.Server["timeout"], + "max_conns": cv.Database["max_conns"], + "min_conns": cv.Database["min_conns"], + } + for field, val := range numericFields { + if _, err := strconv.Atoi(val); err != nil { + return fmt.Errorf("campo %q tem valor inválido: %q", field, val) + } + } + + content, err := GenerateConfigTOML(cv) + if err != nil { + return err + } + + return os.WriteFile(path, []byte(content), 0644) +} diff --git a/internal/tui/docker.go b/internal/tui/docker.go new file mode 100644 index 0000000..e8a1515 --- /dev/null +++ b/internal/tui/docker.go @@ -0,0 +1,96 @@ +package tui + +import ( + "fmt" + "os/exec" + "path/filepath" + "strings" + "time" +) + +func RunContainer(image string, name string, port int) error { + + cmd := exec.Command( + "docker", "run", + "-d", + "--name", name, + "-p", fmt.Sprintf("%d:%d", port, port), + image, + ) + + return cmd.Run() +} + +func PullImage(image string) (string, error) { + + cmd := exec.Command("docker", "pull", image) + + out, err := cmd.CombinedOutput() + return string(out), err +} + +func ImageExists(image string) bool { + cmd := exec.Command("docker", "image", "inspect", image) + err := cmd.Run() + + return err == nil +} + +func PushFileToContainer(container, filePath, destinationPath string) bool { + dest := fmt.Sprintf("%s:%s", container, destinationPath) + + cmd := exec.Command("docker", "cp", filePath, dest) + + err := cmd.Run() + return err == nil +} + +func RunAppClienteContainer(image, containerName, configPath, configDestinationPath string, cv ConfigValues) error { + removeExistingContainer(containerName) + + absPath, err := filepath.Abs(configPath) + if err != nil { + return fmt.Errorf("erro ao resolver caminho absoluto: %w", err) + } + + cmd := exec.Command( + "docker", "run", "-d", + "--name", containerName, + "--network", "app-dono_app", + "--restart", "unless-stopped", + "-v", fmt.Sprintf("%s:%s", absPath, configDestinationPath), // Config mapping + "-v", fmt.Sprintf("%s:/app/certs", cv.Cert["cert_dir_path"]), // Certs mapping + image, + ) + + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("docker run falhou: %w\noutput: %s", err, string(out)) + } + + time.Sleep(2 * time.Second) + containerID := strings.TrimSpace(string(out)) + return verifyContainerRunning(containerID) +} + +func removeExistingContainer(name string) { + exec.Command("docker", "stop", name).Run() + exec.Command("docker", "rm", name).Run() +} + +func verifyContainerRunning(containerID string) error { + cmd := exec.Command("docker", "inspect", "--format", "{{.State.Status}}", containerID) + out, err := cmd.Output() + if err != nil { + return fmt.Errorf("erro ao inspecionar container: %w", err) + } + + status := strings.TrimSpace(string(out)) + if status != "running" { + logCmd := exec.Command("docker", "logs", "--tail", "20", containerID) + logs, _ := logCmd.CombinedOutput() + return fmt.Errorf("container parou com status %q.\nlogs:\n%s", status, string(logs)) + } + + return nil +} diff --git a/internal/tui/form.go b/internal/tui/form.go index a220a68..7af2ef2 100644 --- a/internal/tui/form.go +++ b/internal/tui/form.go @@ -18,12 +18,13 @@ const ( ) type FormField struct { + Id string Label string Placeholder string Default string Type FieldType Options []string // for FieldTypeSelect - optionIdx int // for FieldTypeSelect + OptionIdx int // for FieldTypeSelect CharLimit int input textinput.Model } @@ -41,7 +42,7 @@ func NewFormStep(title string, fields []FormField) FormStep { for j, opt := range f.Fields[i].Options { // Apply default option if opt == f.Fields[i].Default { - f.Fields[i].optionIdx = j + f.Fields[i].OptionIdx = j break } } @@ -81,13 +82,13 @@ func (f *FormStep) Update(msg tea.Msg) (done bool, cmd tea.Cmd) { switch msg.String() { // Select specific inputs case "left", "h": - if isSelect && focused.optionIdx > 0 { - focused.optionIdx-- + if isSelect && focused.OptionIdx > 0 { + focused.OptionIdx-- } case "right", "l": - if isSelect && focused.optionIdx < len(focused.Options)-1 { - focused.optionIdx++ + if isSelect && focused.OptionIdx < len(focused.Options)-1 { + focused.OptionIdx++ } // Generic inputs @@ -120,9 +121,9 @@ func (f *FormStep) Values() map[string]string { out := make(map[string]string) for _, field := range f.Fields { if field.Type == FieldTypeSelect { - out[field.Label] = field.Options[field.optionIdx] + out[field.Id] = field.Options[field.OptionIdx] } else { - out[field.Label] = field.input.Value() + out[field.Id] = field.input.Value() } } return out @@ -155,7 +156,7 @@ func renderField(field FormField, focused bool) string { case FieldTypeSelect: var opts []string for i, opt := range field.Options { - if i == field.optionIdx { + if i == field.OptionIdx { opts = append(opts, SelectedStyle.Render("[ "+opt+" ]")) } else { opts = append(opts, DimStyle.Render(" "+opt+" ")) diff --git a/internal/tui/model.go b/internal/tui/model.go index 3f27844..d2a1cb7 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -1,37 +1,78 @@ package tui import ( + "charm.land/bubbles/v2/spinner" tea "charm.land/bubbletea/v2" ) type Model struct { currentStep step cursor int + width int + height int + spinner spinner.Model + loading bool dockerInstalled bool checkDockerDone bool checkProgress float64 - isPublicIP bool + downloadDone bool + downloadMessage string + downloadError error + loginData DockerLoginData + isPublicIP bool + + loginForm FormStep serverForm FormStep dbForm FormStep certForm FormStep configValues ConfigValues + finishedFile bool + configFileError error + finishedDockerRun bool + dockerRunError error + err error } +type DockerLoginData struct { + Login string + Password string +} + type ConfigValues struct { + Login map[string]string Server map[string]string Database map[string]string Cert map[string]string } func InitialModel() Model { + s := spinner.New() + s.Spinner = spinner.Dot + s.Style = SpinnerStyle + return Model{ currentStep: StepCheckDocker, + loginForm: NewFormStep("Login no Repositório Docker", []FormField{ + { + Id: "user", + Label: "Usuário", + Placeholder: "usuario", + Type: FieldTypeText, + }, + { + Id: "password", + Label: "Senha", + Placeholder: "senhaForte123", + Type: FieldTypePassword, + }, + }), serverForm: NewFormStep("Servidor", []FormField{ { + Id: "port", Label: "Porta", Placeholder: "8081", Default: "8081", @@ -39,6 +80,7 @@ func InitialModel() Model { CharLimit: 4, }, { + Id: "timeout", Label: "Timeout (Segundos)", Placeholder: "30", Default: "30", @@ -46,6 +88,7 @@ func InitialModel() Model { CharLimit: 3, }, { + Id: "environment", Label: "Ambiente", Default: "production", Type: FieldTypeSelect, @@ -54,24 +97,28 @@ func InitialModel() Model { }), dbForm: NewFormStep("Banco de Dados", []FormField{ { + Id: "database_type", Label: "Tipo do Banco", Default: "postgres", Type: FieldTypeSelect, Options: []string{"postgres", "oracle"}, }, { + Id: "database_url", Label: "URL de acesso", Placeholder: "postgres://usuario:senha@banco:5432/app_dono_db", Default: "postgres://usuario:senha@banco:5432/app_dono_db", Type: FieldTypeText, }, { + Id: "max_conns", Label: "Conexões ativas (máximo)", Placeholder: "10", Default: "10", Type: FieldTypeNumber, }, { + Id: "min_conns", Label: "Conexões ativas (mínimo)", Placeholder: "2", Default: "2", @@ -80,33 +127,45 @@ func InitialModel() Model { }), certForm: NewFormStep("Certificado", []FormField{ { - Label: "Caminho para o arquivo do certificado", - Placeholder: "/caminho/para/certificado.crt", - Default: "/caminho/para/certificado.crt", + Id: "cert_dir_path", + Label: "Caminho para o diretório dos certificados (será montado no container)", + Placeholder: "/caminho/para/diretorio", + Default: "/caminho/para/diretorio", + Type: FieldTypeText, + }, + { + Id: "cert_name", + Label: "Nome do arquivo do certificado", + Placeholder: "certificado.crt", + Default: "certificado.crt", Type: FieldTypeText, }, { - Label: "Caminho para o arquivo da chave", - Placeholder: "/caminho/para/chave.key", - Default: "/caminho/para/chave.key", + Id: "key_name", + Label: "Nome do arquivo da chave", + Placeholder: "chave.key", + Default: "chave.key", Type: FieldTypeText, }, { - Label: "Caminho para o arquivo da autoridade certificadora", - Placeholder: "/caminho/para/chaveCA.crt", - Default: "/caminho/para/chaveCA.crt", + Id: "ca_name", + Label: "Nome do arquivo da autoridade certificadora", + Placeholder: "chaveCA.crt", + Default: "chaveCA.crt", Type: FieldTypeText, }, { + Id: "server_name", Label: "Nome do servidor", Placeholder: "client", Default: "client", Type: FieldTypeText, }, }), + spinner: s, } } func (m Model) Init() tea.Cmd { - return tea.Batch(CheckDockerCmd(), tickCmd()) + return tea.Batch(CheckDockerCmd(), TickCmd(), m.spinner.Tick) } diff --git a/internal/tui/steps.go b/internal/tui/steps.go index b2c4242..fa6ea51 100644 --- a/internal/tui/steps.go +++ b/internal/tui/steps.go @@ -3,16 +3,25 @@ package tui type step int const ( + // Docker Validation StepCheckDocker step = iota StepDockerInstall + + // Image Fetching + StepDockerLogin StepDownloadImage + // IP Stuff StepIPQuestion StepInstallWireguard + // Docker Config StepServerConfig StepDatabaseConfig StepCertConfig + // Finalizing + StepGenerateFile + StepRunDocker StepDone ) diff --git a/internal/tui/styles.go b/internal/tui/styles.go index a326280..f8b8fc6 100644 --- a/internal/tui/styles.go +++ b/internal/tui/styles.go @@ -53,4 +53,7 @@ var ( Foreground(lipgloss.Color(secondary)). Bold(true). Border(lipgloss.NormalBorder(), false, false, true) + + SpinnerStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("205")) ) diff --git a/internal/tui/update.go b/internal/tui/update.go index 1a8ba5c..cae3fc1 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -1,11 +1,18 @@ package tui import ( + "fmt" "math/rand" + "charm.land/bubbles/v2/spinner" tea "charm.land/bubbletea/v2" ) +const ( + imageName = "hub.davinti.com.br:443/app-dono/app-cliente:latest" + configPath = "config.toml" +) + func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if key, ok := msg.(tea.KeyPressMsg); ok { switch key.String() { @@ -14,11 +21,32 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + case spinner.TickMsg: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + } + switch m.currentStep { case StepCheckDocker: return m.updateCheckDocker(msg) case StepDockerInstall: return m.updateDockerInstall(msg) + case StepDockerLogin: + done, cmd := m.loginForm.Update(msg) + + if done { + m.configValues.Login = m.loginForm.Values() + m.currentStep = StepDownloadImage + + return m, DownloadImageCmd(m.configValues.Login["user"], m.configValues.Login["password"]) + } + + return m, cmd case StepDownloadImage: return m.updateDownloadImage(msg) case StepIPQuestion: @@ -29,6 +57,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { done, cmd := m.serverForm.Update(msg) if done { + m.configValues.Server = m.serverForm.Values() m.currentStep = StepDatabaseConfig } @@ -37,6 +66,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { done, cmd := m.dbForm.Update(msg) if done { + m.configValues.Database = m.dbForm.Values() m.currentStep = StepCertConfig } @@ -45,10 +75,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { done, cmd := m.certForm.Update(msg) if done { - m.currentStep = StepDone + m.configValues.Cert = m.certForm.Values() + m.currentStep = StepGenerateFile + + return m, GenerateConfigFile(m.configValues, configPath) } return m, cmd + case StepGenerateFile: + return m.updateGenerateFile(msg) + case StepRunDocker: + return m.updateRunDocker(msg) case StepDone: return m, tea.Quit } @@ -61,19 +98,19 @@ func (m Model) updateCheckDocker(msg tea.Msg) (tea.Model, tea.Cmd) { case DockerCheckedMsg: m.dockerInstalled = msg.Installed m.checkDockerDone = true - case TickMsg: if m.checkProgress < 1.0 { - m.checkProgress += 0.15 + (rand.Float64()*0.15 - 0.15) + m.checkProgress += 0.2 + (rand.Float64()*0.2 - 0.2) if m.checkProgress > 1.0 { m.checkProgress = 1.0 } - return m, tickCmd() + return m, TickCmd() } case tea.KeyPressMsg: - if m.checkDockerDone { + if m.checkDockerDone && m.checkProgress == 1 { if m.dockerInstalled { - m.currentStep = StepIPQuestion + m.loading = true + m.currentStep = StepDockerLogin } else { m.currentStep = StepDockerInstall } @@ -84,14 +121,27 @@ func (m Model) updateCheckDocker(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m Model) updateDockerInstall(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { + switch msg.(type) { case tea.KeyPressMsg: return m, tea.Quit - case DockerInstalledMsg: - if msg.Err != nil { - m.err = msg.Err - } else { + } + + return m, nil +} + +func (m Model) updateDownloadImage(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case ImageDownloadFinishedMsg: + m.downloadDone = true + m.downloadMessage = msg.Message + m.downloadError = msg.Err + return m, nil + + case tea.KeyPressMsg: + if m.downloadDone && m.downloadError == nil { m.currentStep = StepIPQuestion + } else if m.downloadDone { + return m, tea.Quit } } @@ -127,10 +177,43 @@ func (m Model) updateIPQuestion(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } -func (m Model) updateWaitForProgramQuit(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg.(type) { +func (m Model) updateGenerateFile(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case ConfigFileMsg: + m.finishedFile = true + m.configFileError = msg.Err + case tea.KeyPressMsg: - return m, tea.Quit + if m.finishedFile && m.configFileError != nil { + return m, tea.Quit + } else if m.finishedFile && m.configFileError == nil { + m.currentStep = StepRunDocker + + return m, RunAppContainer( + imageName, + "app-dono-cliente", + configPath, + fmt.Sprintf("/app/%s", configPath), + m.configValues, + ) + } + } + + return m, nil +} + +func (m Model) updateRunDocker(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case DockerRunMsg: + m.finishedDockerRun = true + m.dockerRunError = msg.Err + + case tea.KeyPressMsg: + if m.finishedDockerRun && m.dockerRunError != nil { + return m, tea.Quit + } else if m.finishedDockerRun && m.dockerRunError == nil { + m.currentStep = StepDone + } } return m, nil diff --git a/internal/tui/view.go b/internal/tui/view.go index a3fc738..604c462 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -5,34 +5,63 @@ import ( "strings" tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" ) const ( - header = "App do Dono - Instalador Cliente" + header = "App do Dono - Instalador Cliente" + defaultMsg = "ctrl+c: sair" + anyKeyOutMsg = "qualquer tecla: sair" + formMsg = "tab: próximo campo • enter: confirmar • ctrl+c: sair" ) func (m Model) View() tea.View { pad := strings.Repeat(" ", padding) var body string - helpMsg := "ctrl+c: sair" + helpMsg := defaultMsg switch m.currentStep { + // Docker stuff case StepCheckDocker: body = m.viewCheckDocker() case StepDockerInstall: body = m.viewDockerInstall() - helpMsg = "qualquer tecla: sair" + helpMsg = anyKeyOutMsg + case StepDockerLogin: + body = m.loginForm.View() + helpMsg = formMsg + case StepDownloadImage: + body = m.viewDownloadImage() + + // IP Stuff case StepIPQuestion: body = m.viewIPQuestion() + + // App Config Stuff case StepServerConfig: body = m.serverForm.View() - helpMsg = "tab: próximo campo • enter: confirmar • ctrl+c: sair" + helpMsg = formMsg case StepDatabaseConfig: body = m.dbForm.View() - helpMsg = "tab: próximo campo • enter: confirmar • ctrl+c: sair" + helpMsg = formMsg case StepCertConfig: body = m.certForm.View() - helpMsg = "tab: próximo campo • enter: confirmar • ctrl+c: sair" + helpMsg = formMsg + + // Finalize + case StepGenerateFile: + body = m.viewGenerateFile() + if m.finishedFile && m.configFileError != nil { + helpMsg = anyKeyOutMsg + } + case StepRunDocker: + body = m.viewDockerRun() + if m.finishedDockerRun && m.dockerRunError != nil { + helpMsg = anyKeyOutMsg + } + case StepDone: + body = m.viewDoneMessage() + helpMsg = anyKeyOutMsg } help := HelpStyle.Render(helpMsg) @@ -88,6 +117,32 @@ func (m Model) viewDockerInstall() string { 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 qualquer tecla para sair.") + } + + return sb.String() +} + func (m Model) viewIPQuestion() string { pad := strings.Repeat(" ", padding) @@ -113,3 +168,63 @@ func (m Model) viewIPQuestion() string { 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 + "Tente novamente.") + } + } + + 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 + "Tente novamente.") + } + } + + 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() +}