| @ -0,0 +1,27 @@ | |||
| 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 | |||
| } | |||
| @ -0,0 +1,47 @@ | |||
| package tui | |||
| import ( | |||
| "os/exec" | |||
| "time" | |||
| tea "charm.land/bubbletea/v2" | |||
| ) | |||
| // --- Messages --- | |||
| // Docker installation check | |||
| type DockerCheckedMsg struct{ Installed bool } | |||
| type DockerInstalledMsg struct{ Err error } | |||
| type TickMsg struct{} | |||
| // Image downloading | |||
| type ImageDownloadFinishedMsg struct { | |||
| Err error | |||
| } | |||
| type DownloadTickMsg struct{} | |||
| // --- Commands --- | |||
| func CheckDockerCmd() tea.Cmd { | |||
| return func() tea.Msg { | |||
| _, err := exec.LookPath("docker") | |||
| return DockerCheckedMsg{Installed: err == nil} | |||
| } | |||
| } | |||
| func tickCmd() tea.Cmd { | |||
| return tea.Tick(100*time.Millisecond, func(t time.Time) tea.Msg { | |||
| return TickMsg{} | |||
| }) | |||
| } | |||
| func downloadImageCmd() tea.Cmd { | |||
| return func() tea.Msg { | |||
| err := docker.PullImage("hub.davinti.com.br:443/app-dono/app-cliente:latest") | |||
| return ImageDownloadFinishedMsg{ | |||
| Err: err, | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,186 @@ | |||
| package tui | |||
| import ( | |||
| "fmt" | |||
| "strings" | |||
| "charm.land/bubbles/v2/textinput" | |||
| tea "charm.land/bubbletea/v2" | |||
| ) | |||
| type FieldType int | |||
| const ( | |||
| FieldTypeText FieldType = iota | |||
| FieldTypeSelect | |||
| FieldTypePassword | |||
| FieldTypeNumber | |||
| ) | |||
| type FormField struct { | |||
| Label string | |||
| Placeholder string | |||
| Default string | |||
| Type FieldType | |||
| Options []string // for FieldTypeSelect | |||
| optionIdx int // for FieldTypeSelect | |||
| CharLimit int | |||
| input textinput.Model | |||
| } | |||
| type FormStep struct { | |||
| Title string | |||
| Fields []FormField | |||
| focus int | |||
| } | |||
| func NewFormStep(title string, fields []FormField) FormStep { | |||
| f := FormStep{Title: title, Fields: fields} | |||
| for i := range f.Fields { | |||
| if f.Fields[i].Type == FieldTypeSelect { | |||
| for j, opt := range f.Fields[i].Options { | |||
| // Apply default option | |||
| if opt == f.Fields[i].Default { | |||
| f.Fields[i].optionIdx = j | |||
| break | |||
| } | |||
| } | |||
| continue | |||
| } | |||
| ti := textinput.New() | |||
| ti.Placeholder = f.Fields[i].Placeholder | |||
| if f.Fields[i].Default != "" { | |||
| ti.SetValue(f.Fields[i].Default) | |||
| } | |||
| if f.Fields[i].CharLimit > 0 { | |||
| ti.CharLimit = f.Fields[i].CharLimit | |||
| } | |||
| if f.Fields[i].Type == FieldTypePassword { | |||
| ti.EchoMode = textinput.EchoPassword | |||
| } | |||
| if i == 0 { | |||
| ti.Focus() | |||
| } | |||
| f.Fields[i].input = ti | |||
| } | |||
| return f | |||
| } | |||
| // Update | |||
| func (f *FormStep) Update(msg tea.Msg) (done bool, cmd tea.Cmd) { | |||
| focused := &f.Fields[f.focus] | |||
| isSelect := focused.Type == FieldTypeSelect | |||
| switch msg := msg.(type) { | |||
| case tea.KeyPressMsg: | |||
| switch msg.String() { | |||
| // Select specific inputs | |||
| case "left", "h": | |||
| if isSelect && focused.optionIdx > 0 { | |||
| focused.optionIdx-- | |||
| } | |||
| case "right", "l": | |||
| if isSelect && focused.optionIdx < len(focused.Options)-1 { | |||
| focused.optionIdx++ | |||
| } | |||
| // Generic inputs | |||
| case "tab", "down": | |||
| f.blurCurrent() | |||
| f.focus = (f.focus + 1) % len(f.Fields) | |||
| f.focusCurrent() | |||
| case "shift+tab", "up": | |||
| f.blurCurrent() | |||
| f.focus = (f.focus - 1 + len(f.Fields)) % len(f.Fields) | |||
| f.focusCurrent() | |||
| case "enter": | |||
| if f.focus == len(f.Fields)-1 { | |||
| return true, nil | |||
| } | |||
| f.blurCurrent() | |||
| f.focus++ | |||
| f.focusCurrent() | |||
| } | |||
| } | |||
| var c tea.Cmd | |||
| f.Fields[f.focus].input, c = f.Fields[f.focus].input.Update(msg) | |||
| return false, c | |||
| } | |||
| 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] | |||
| } else { | |||
| out[field.Label] = field.input.Value() | |||
| } | |||
| } | |||
| return out | |||
| } | |||
| // View | |||
| func (f FormStep) View() string { | |||
| pad := strings.Repeat(" ", padding) | |||
| var sb strings.Builder | |||
| sb.WriteString(pad + TitleStyle.Render(f.Title) + "") | |||
| for i, field := range f.Fields { | |||
| sb.WriteString(renderField(field, i == f.focus)) | |||
| } | |||
| return sb.String() | |||
| } | |||
| func renderField(field FormField, focused bool) string { | |||
| pad := strings.Repeat(" ", padding) | |||
| label := " " + field.Label | |||
| if focused { | |||
| label = CursorStyle.Render("> " + field.Label) | |||
| } | |||
| var value string | |||
| switch field.Type { | |||
| case FieldTypeSelect: | |||
| var opts []string | |||
| for i, opt := range field.Options { | |||
| if i == field.optionIdx { | |||
| opts = append(opts, SelectedStyle.Render("[ "+opt+" ]")) | |||
| } else { | |||
| opts = append(opts, DimStyle.Render(" "+opt+" ")) | |||
| } | |||
| } | |||
| value = strings.Join(opts, " ") | |||
| if focused { | |||
| value += HelpStyle.Render(" ← →") | |||
| } | |||
| default: | |||
| value = field.input.View() | |||
| } | |||
| return fmt.Sprintf("\n\n%s%s\n%s %s", pad, label, pad, value) | |||
| } | |||
| // Helpers | |||
| func (f *FormStep) blurCurrent() { | |||
| if f.Fields[f.focus].Type != FieldTypeSelect { | |||
| f.Fields[f.focus].input.Blur() | |||
| } | |||
| } | |||
| func (f *FormStep) focusCurrent() { | |||
| if f.Fields[f.focus].Type != FieldTypeSelect { | |||
| f.Fields[f.focus].input.Focus() | |||
| } | |||
| } | |||
| @ -0,0 +1,112 @@ | |||
| package tui | |||
| import ( | |||
| tea "charm.land/bubbletea/v2" | |||
| ) | |||
| type Model struct { | |||
| currentStep step | |||
| cursor int | |||
| dockerInstalled bool | |||
| checkDockerDone bool | |||
| checkProgress float64 | |||
| isPublicIP bool | |||
| serverForm FormStep | |||
| dbForm FormStep | |||
| certForm FormStep | |||
| configValues ConfigValues | |||
| err error | |||
| } | |||
| type ConfigValues struct { | |||
| Server map[string]string | |||
| Database map[string]string | |||
| Cert map[string]string | |||
| } | |||
| func InitialModel() Model { | |||
| return Model{ | |||
| currentStep: StepCheckDocker, | |||
| serverForm: NewFormStep("Servidor", []FormField{ | |||
| { | |||
| Label: "Porta", | |||
| Placeholder: "8081", | |||
| Default: "8081", | |||
| Type: FieldTypeNumber, | |||
| CharLimit: 4, | |||
| }, | |||
| { | |||
| Label: "Timeout (Segundos)", | |||
| Placeholder: "30", | |||
| Default: "30", | |||
| Type: FieldTypeNumber, | |||
| CharLimit: 3, | |||
| }, | |||
| { | |||
| Label: "Ambiente", | |||
| Default: "production", | |||
| Type: FieldTypeSelect, | |||
| Options: []string{"development", "production"}, | |||
| }, | |||
| }), | |||
| dbForm: NewFormStep("Banco de Dados", []FormField{ | |||
| { | |||
| Label: "Tipo do Banco", | |||
| Default: "postgres", | |||
| Type: FieldTypeSelect, | |||
| Options: []string{"postgres", "oracle"}, | |||
| }, | |||
| { | |||
| Label: "URL de acesso", | |||
| Placeholder: "postgres://usuario:senha@banco:5432/app_dono_db", | |||
| Default: "postgres://usuario:senha@banco:5432/app_dono_db", | |||
| Type: FieldTypeText, | |||
| }, | |||
| { | |||
| Label: "Conexões ativas (máximo)", | |||
| Placeholder: "10", | |||
| Default: "10", | |||
| Type: FieldTypeNumber, | |||
| }, | |||
| { | |||
| Label: "Conexões ativas (mínimo)", | |||
| Placeholder: "2", | |||
| Default: "2", | |||
| Type: FieldTypeNumber, | |||
| }, | |||
| }), | |||
| certForm: NewFormStep("Certificado", []FormField{ | |||
| { | |||
| Label: "Caminho para o arquivo do certificado", | |||
| Placeholder: "/caminho/para/certificado.crt", | |||
| Default: "/caminho/para/certificado.crt", | |||
| Type: FieldTypeText, | |||
| }, | |||
| { | |||
| Label: "Caminho para o arquivo da chave", | |||
| Placeholder: "/caminho/para/chave.key", | |||
| Default: "/caminho/para/chave.key", | |||
| Type: FieldTypeText, | |||
| }, | |||
| { | |||
| Label: "Caminho para o arquivo da autoridade certificadora", | |||
| Placeholder: "/caminho/para/chaveCA.crt", | |||
| Default: "/caminho/para/chaveCA.crt", | |||
| Type: FieldTypeText, | |||
| }, | |||
| { | |||
| Label: "Nome do servidor", | |||
| Placeholder: "client", | |||
| Default: "client", | |||
| Type: FieldTypeText, | |||
| }, | |||
| }), | |||
| } | |||
| } | |||
| func (m Model) Init() tea.Cmd { | |||
| return tea.Batch(CheckDockerCmd(), tickCmd()) | |||
| } | |||
| @ -0,0 +1,56 @@ | |||
| package tui | |||
| import "charm.land/lipgloss/v2" | |||
| const padding = 2 | |||
| const ( | |||
| primary = "#005F87" // Deep Sea Blue | |||
| secondary = "#00D7FF" // Cyan/Data highlight | |||
| success = "#06D6A0" // Emerald Green | |||
| warning = "#FFD166" // Soft Gold | |||
| danger = "#EF476F" // Muted Crimson | |||
| neutral = "#8E9AAF" // Slate Gray | |||
| darkGray = "#353B48" | |||
| ) | |||
| var ( | |||
| // Core Text Styles | |||
| TitleStyle = lipgloss.NewStyle(). | |||
| Foreground(lipgloss.Color(primary)). | |||
| Bold(true). | |||
| MarginBottom(1) | |||
| CursorStyle = lipgloss.NewStyle(). | |||
| Foreground(lipgloss.Color(secondary)) | |||
| HelpStyle = lipgloss.NewStyle(). | |||
| Foreground(lipgloss.Color("#626262")) | |||
| // Status Styles | |||
| ErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(danger)) | |||
| InfoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(primary)) | |||
| WarnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color(warning)) | |||
| // Form & Selection | |||
| SelectedStyle = lipgloss.NewStyle(). | |||
| Foreground(lipgloss.Color(secondary)). | |||
| Bold(true). | |||
| PaddingLeft(1) | |||
| DimStyle = lipgloss.NewStyle(). | |||
| Foreground(lipgloss.Color("#4A4A4A")) | |||
| // Data Components (Progress / Charts) | |||
| ProgressFillStyle = lipgloss.NewStyle(). | |||
| Foreground(lipgloss.Color(primary)) | |||
| ProgressEmptyStyle = lipgloss.NewStyle(). | |||
| Foreground(lipgloss.Color(darkGray)) | |||
| // Table Styles | |||
| HeaderStyle = lipgloss.NewStyle(). | |||
| Foreground(lipgloss.Color(secondary)). | |||
| Bold(true). | |||
| Border(lipgloss.NormalBorder(), false, false, true) | |||
| ) | |||
| @ -0,0 +1,137 @@ | |||
| package tui | |||
| import ( | |||
| "math/rand" | |||
| tea "charm.land/bubbletea/v2" | |||
| ) | |||
| func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| if key, ok := msg.(tea.KeyPressMsg); ok { | |||
| switch key.String() { | |||
| case "ctrl+c": | |||
| return m, tea.Quit | |||
| } | |||
| } | |||
| switch m.currentStep { | |||
| case StepCheckDocker: | |||
| return m.updateCheckDocker(msg) | |||
| case StepDockerInstall: | |||
| return m.updateDockerInstall(msg) | |||
| case StepDownloadImage: | |||
| return m.updateDownloadImage(msg) | |||
| case StepIPQuestion: | |||
| return m.updateIPQuestion(msg) | |||
| //case StepInstallWireguard | |||
| // return m.updateInstallWireguard(msg) | |||
| case StepServerConfig: | |||
| done, cmd := m.serverForm.Update(msg) | |||
| if done { | |||
| m.currentStep = StepDatabaseConfig | |||
| } | |||
| return m, cmd | |||
| case StepDatabaseConfig: | |||
| done, cmd := m.dbForm.Update(msg) | |||
| if done { | |||
| m.currentStep = StepCertConfig | |||
| } | |||
| return m, cmd | |||
| case StepCertConfig: | |||
| done, cmd := m.certForm.Update(msg) | |||
| if done { | |||
| m.currentStep = StepDone | |||
| } | |||
| return m, cmd | |||
| case StepDone: | |||
| return m, tea.Quit | |||
| } | |||
| return m, nil | |||
| } | |||
| func (m Model) updateCheckDocker(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| switch msg := msg.(type) { | |||
| 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) | |||
| if m.checkProgress > 1.0 { | |||
| m.checkProgress = 1.0 | |||
| } | |||
| return m, tickCmd() | |||
| } | |||
| case tea.KeyPressMsg: | |||
| if m.checkDockerDone { | |||
| if m.dockerInstalled { | |||
| m.currentStep = StepIPQuestion | |||
| } else { | |||
| m.currentStep = StepDockerInstall | |||
| } | |||
| } | |||
| } | |||
| return m, nil | |||
| } | |||
| func (m Model) updateDockerInstall(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| switch msg := msg.(type) { | |||
| case tea.KeyPressMsg: | |||
| return m, tea.Quit | |||
| case DockerInstalledMsg: | |||
| if msg.Err != nil { | |||
| m.err = msg.Err | |||
| } else { | |||
| m.currentStep = StepIPQuestion | |||
| } | |||
| } | |||
| return m, nil | |||
| } | |||
| func (m Model) updateIPQuestion(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| numOptions := 2 | |||
| switch msg := msg.(type) { | |||
| case tea.KeyPressMsg: | |||
| switch msg.String() { | |||
| case "up", "k": | |||
| if m.cursor > 0 { | |||
| m.cursor-- | |||
| } | |||
| case "down", "j": | |||
| if m.cursor < numOptions-1 { | |||
| m.cursor++ | |||
| } | |||
| case "enter": | |||
| // Yes | |||
| if m.cursor == 0 { | |||
| m.currentStep = StepServerConfig | |||
| return m, nil | |||
| } | |||
| m.currentStep = StepInstallWireguard | |||
| return m, nil | |||
| } | |||
| } | |||
| return m, nil | |||
| } | |||
| func (m Model) updateWaitForProgramQuit(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| switch msg.(type) { | |||
| case tea.KeyPressMsg: | |||
| return m, tea.Quit | |||
| } | |||
| return m, nil | |||
| } | |||
| @ -0,0 +1,115 @@ | |||
| package tui | |||
| import ( | |||
| "fmt" | |||
| "strings" | |||
| tea "charm.land/bubbletea/v2" | |||
| ) | |||
| const ( | |||
| header = "App do Dono - Instalador Cliente" | |||
| ) | |||
| func (m Model) View() tea.View { | |||
| pad := strings.Repeat(" ", padding) | |||
| var body string | |||
| helpMsg := "ctrl+c: sair" | |||
| switch m.currentStep { | |||
| case StepCheckDocker: | |||
| body = m.viewCheckDocker() | |||
| case StepDockerInstall: | |||
| body = m.viewDockerInstall() | |||
| helpMsg = "qualquer tecla: sair" | |||
| case StepIPQuestion: | |||
| body = m.viewIPQuestion() | |||
| case StepServerConfig: | |||
| body = m.serverForm.View() | |||
| helpMsg = "tab: próximo campo • enter: confirmar • ctrl+c: sair" | |||
| case StepDatabaseConfig: | |||
| body = m.dbForm.View() | |||
| helpMsg = "tab: próximo campo • enter: confirmar • ctrl+c: sair" | |||
| case StepCertConfig: | |||
| body = m.certForm.View() | |||
| helpMsg = "tab: próximo campo • enter: confirmar • ctrl+c: sair" | |||
| } | |||
| help := HelpStyle.Render(helpMsg) | |||
| v := tea.NewView(fmt.Sprintf("\n%s%s\n\n%s\n\n%s%s\n", | |||
| pad, TitleStyle.Render(header), | |||
| 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) 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() | |||
| } | |||
| @ -1,28 +0,0 @@ | |||
| package tui | |||
| import ( | |||
| "os/exec" | |||
| tea "charm.land/bubbletea/v2" | |||
| ) | |||
| // --- Messages --- | |||
| type DockerCheckedMsg struct{ Installed bool } | |||
| type DockerInstalledMsg struct{ Err error } | |||
| // --- Commands --- | |||
| func CheckDockerCmd() tea.Cmd { | |||
| return func() tea.Msg { | |||
| _, err := exec.LookPath("docker") | |||
| return DockerCheckedMsg{Installed: err == nil} | |||
| } | |||
| } | |||
| func InstallDockerCmd() tea.Cmd { | |||
| return func() tea.Msg { | |||
| err := exec.Command("sh", "-c", "curl -fsSL https://get.docker.com | sh").Run() | |||
| return DockerInstalledMsg{Err: err} | |||
| } | |||
| } | |||
| @ -1 +0,0 @@ | |||
| package tui | |||
| @ -1,25 +0,0 @@ | |||
| package tui | |||
| import ( | |||
| "charm.land/bubbles/v2/textinput" | |||
| tea "charm.land/bubbletea/v2" | |||
| ) | |||
| type Model struct { | |||
| currentStep step | |||
| dockerInstalled bool | |||
| isPublicIP bool | |||
| inputs []textinput.Model | |||
| cursor int | |||
| err error | |||
| } | |||
| func InitialModel() Model { | |||
| return Model{ | |||
| currentStep: StepCheckDocker, | |||
| } | |||
| } | |||
| func (m Model) Init() tea.Cmd { | |||
| return CheckDockerCmd() | |||
| } | |||
| @ -1,12 +0,0 @@ | |||
| package tui | |||
| import "charm.land/lipgloss/v2" | |||
| const padding = 2 | |||
| var ( | |||
| TitleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#FAFAFA")) | |||
| HelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")) | |||
| CursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF75B7")) | |||
| ErrorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5555")) | |||
| ) | |||
| @ -1,76 +0,0 @@ | |||
| package tui | |||
| import tea "charm.land/bubbletea/v2" | |||
| func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| // Global keys first — always handled regardless of step | |||
| if key, ok := msg.(tea.KeyPressMsg); ok { | |||
| switch key.String() { | |||
| case "ctrl+c", "q": | |||
| return m, tea.Quit | |||
| } | |||
| } | |||
| switch m.currentStep { | |||
| case StepCheckDocker: | |||
| return m.updateCheckDocker(msg) | |||
| case StepDockerInstall: | |||
| return m.updateDockerInstall(msg) | |||
| case StepIPQuestion: | |||
| return m.updateIPQuestion(msg) | |||
| case StepDone: | |||
| return m, tea.Quit | |||
| } | |||
| return m, nil | |||
| } | |||
| func (m Model) updateCheckDocker(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| if msg, ok := msg.(DockerCheckedMsg); ok { | |||
| m.dockerInstalled = msg.Installed | |||
| if msg.Installed { | |||
| m.currentStep = StepIPQuestion | |||
| } else { | |||
| m.currentStep = StepDockerInstall | |||
| m.cursor = 0 // reset cursor on enter | |||
| } | |||
| } | |||
| return m, nil | |||
| } | |||
| func (m Model) updateDockerInstall(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| const numOptions = 2 | |||
| switch msg := msg.(type) { | |||
| case tea.KeyPressMsg: | |||
| switch msg.String() { | |||
| case "up", "k": | |||
| if m.cursor > 0 { | |||
| m.cursor-- | |||
| } | |||
| case "down", "j": | |||
| if m.cursor < numOptions-1 { | |||
| m.cursor++ | |||
| } | |||
| case "enter": | |||
| switch m.cursor { | |||
| case 0: | |||
| return m, InstallDockerCmd() | |||
| case 1: | |||
| return m, tea.Quit | |||
| } | |||
| } | |||
| case DockerInstalledMsg: | |||
| if msg.Err != nil { | |||
| m.err = msg.Err | |||
| } else { | |||
| m.currentStep = StepIPQuestion | |||
| } | |||
| } | |||
| return m, nil | |||
| } | |||
| func (m Model) updateIPQuestion(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| return m, nil | |||
| } | |||
| @ -1,65 +0,0 @@ | |||
| package tui | |||
| import ( | |||
| "fmt" | |||
| "strings" | |||
| tea "charm.land/bubbletea/v2" | |||
| ) | |||
| const header = "App do Dono - Instalador Cliente" | |||
| func (m Model) View() tea.View { | |||
| pad := strings.Repeat(" ", padding) | |||
| var body string | |||
| switch m.currentStep { | |||
| case StepCheckDocker: | |||
| body = m.viewCheckDocker() | |||
| case StepDockerInstall: | |||
| body = m.viewDockerInstall() | |||
| case StepIPQuestion: | |||
| body = m.viewIPQuestion() | |||
| } | |||
| help := HelpStyle.Render("Pressione q ou ctrl+c para sair") | |||
| return tea.NewView(fmt.Sprintf("\n%s%s\n\n%s\n\n%s%s\n", | |||
| pad, TitleStyle.Render(header), | |||
| body, | |||
| pad, help, | |||
| )) | |||
| } | |||
| func (m Model) viewCheckDocker() string { | |||
| pad := strings.Repeat(" ", padding) | |||
| return pad + "Avaliando instalação do Docker. Aguarde..." | |||
| } | |||
| func (m Model) viewDockerInstall() string { | |||
| pad := strings.Repeat(" ", padding) | |||
| options := []string{"Instalar automaticamente", "Instalar manualmente"} | |||
| var sb strings.Builder | |||
| sb.WriteString(pad + "Nenhuma versão do Docker encontrada.\n") | |||
| sb.WriteString(pad + "Deseja:\n\n") | |||
| for i, opt := range options { | |||
| cursor := " " | |||
| if m.cursor == i { | |||
| cursor = CursorStyle.Render("> ") | |||
| } | |||
| sb.WriteString(pad + cursor + opt + "\n") | |||
| } | |||
| if m.err != nil { | |||
| sb.WriteString("\n" + pad + ErrorStyle.Render("Erro: "+m.err.Error())) | |||
| } | |||
| return sb.String() | |||
| } | |||
| func (m Model) viewIPQuestion() string { | |||
| pad := strings.Repeat(" ", padding) | |||
| return pad + "Existe IP público disponível para a máquina?" | |||
| } | |||