| @ -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?" | |||||
| } | |||||