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" wireguardImageName = "hub.davinti.com.br:443/davinti-vproxy:latest" updaterImageName = "docker:cli" configPath = "config.toml" wireguardConfigPath = "envs" ) 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 case "esc": // Global "back" on input steps. Action/wait steps handle esc themselves. if m.canGoBack() { m.goBack() return m, nil } } } switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height // Fit inputs to the terminal: subtract the left padding, the "> " prompt and // the cursor cell, with a sensible floor for very narrow terminals. inputWidth := msg.Width - (padding * 2) - 4 if inputWidth < 20 { inputWidth = 20 } for _, f := range []*FormStep{ &m.loginForm, &m.wireguardForm, &m.appForm, &m.serverForm, &m.dbForm, &m.certForm, } { f.SetWidth(inputWidth) } 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.advance(StepDownloadImage) return m, DownloadImageCmd(imageName, m.configValues.Login["user"], m.configValues.Login["password"]) } return m, cmd case StepDownloadImage: return m.updateDownloadImage(msg) // Ip & Wireguard case StepIPQuestion: return m.updateIPQuestion(msg) case StepWireguardConfig: done, cmd := m.wireguardForm.Update(msg) if done { m.configValues.Wireguard = m.wireguardForm.Values() m.downloadDone = false m.downloadMessage = "" m.downloadError = nil m.advance(StepGenerateWireguardFile) return m, GenerateWireguardConfigFile(m.configValues, wireguardConfigPath) } return m, cmd case StepGenerateWireguardFile: return m.updateGenerateWireguardFile(msg) case StepDownloadWireguard: return m.updateDownloadWireguard(msg) case StepRunWireguard: return m.updateRunWireguardDocker(msg) case StepAppConfig: done, cmd := m.appForm.Update(msg) if done { m.configValues.Application = m.appForm.Values() m.advance(StepServerConfig) } return m, cmd case StepServerConfig: done, cmd := m.serverForm.Update(msg) if done { m.configValues.Server = m.serverForm.Values() m.advance(StepDatabaseConfig) } return m, cmd case StepDatabaseConfig: done, cmd := m.dbForm.Update(msg) if done { m.configValues.Database = m.dbForm.Values() m.advance(StepCertConfig) } return m, cmd case StepCertConfig: done, cmd := m.certForm.Update(msg) if done { m.configValues.Cert = m.certForm.Values() m.advance(StepReview) } return m, cmd case StepReview: return m.updateReview(msg) case StepGenerateFile: return m.updateGenerateFile(msg) case StepRunDocker: return m.updateRunDocker(msg) case StepRunUpdater: return m.updateRunUpdater(msg) 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.2 + (rand.Float64()*0.2 - 0.2) if m.checkProgress > 1.0 { m.checkProgress = 1.0 } return m, TickCmd() } case tea.KeyPressMsg: if m.checkDockerDone && m.checkProgress == 1 { if m.dockerInstalled { m.loading = true m.advance(StepDockerLogin) } else { m.advance(StepDockerInstall) } } } return m, nil } func (m Model) updateDockerInstall(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.(type) { case tea.KeyPressMsg: return m, tea.Quit } 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.advance(StepIPQuestion) } else if m.downloadDone { switch msg.String() { case "r": m.downloadDone = false m.downloadMessage = "" m.downloadError = nil return m, DownloadImageCmd(imageName, m.configValues.Login["user"], m.configValues.Login["password"]) default: return m, tea.Quit } } } 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.advance(StepAppConfig) return m, nil } m.advance(StepWireguardConfig) } } return m, nil } func (m Model) updateDownloadWireguard(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case ImageDownloadFinishedMsg: m.downloadDone = true m.downloadMessage = msg.Message m.downloadError = msg.Err if m.downloadError == nil { m.currentStep = StepRunWireguard return m, RunWireguardContainer(wireguardConfigPath, m.configValues) } case tea.KeyPressMsg: if m.downloadDone && m.downloadError == nil { m.currentStep = StepRunWireguard } else if m.downloadDone { switch msg.String() { case "r": m.downloadDone = false m.downloadMessage = "" m.downloadError = nil return m, DownloadImageCmd(wireguardImageName, m.configValues.Login["user"], m.configValues.Login["password"]) default: return m, tea.Quit } } } return m, nil } func (m Model) updateGenerateWireguardFile(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case ConfigFileMsg: m.finishedFile = true m.configFileError = msg.Err if msg.Err == nil { m.currentStep = StepDownloadWireguard return m, DownloadImageCmd(wireguardImageName, m.configValues.Login["user"], m.configValues.Login["password"]) } case tea.KeyPressMsg: if m.finishedFile && m.configFileError != nil { // Validation error (e.g. MTU): go back to the form to correct it. The form // is the top of the history stack, so goBack lands on it without leaving a // duplicate entry; fall back to a direct set if history is unexpectedly empty. if !m.goBack() { m.currentStep = StepWireguardConfig } } else if m.finishedFile && m.configFileError == nil { m.currentStep = StepDownloadWireguard return m, DownloadImageCmd(wireguardImageName, m.configValues.Login["user"], m.configValues.Login["password"]) } } return m, nil } func (m Model) updateRunWireguardDocker(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 { switch msg.String() { case "r": m.finishedDockerRun = false m.dockerRunError = nil return m, RunWireguardContainer(wireguardConfigPath, m.configValues) default: return m, tea.Quit } } else if m.finishedDockerRun && m.dockerRunError == nil { m.advance(StepAppConfig) } } return m, nil } // updateReview shows the collected configuration for confirmation before any files are // written. Enter confirms and generates config.toml; Esc (handled globally) goes back // to the previous form to edit. func (m Model) updateReview(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyPressMsg: switch msg.String() { case "enter": m.advance(StepGenerateFile) m.finishedFile = false m.configFileError = nil return m, GenerateConfigFile(m.configValues, configPath) } } return m, nil } 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: if m.finishedFile && m.configFileError != nil { // Validation error: return to the review (top of the stack) so the user can // navigate back to the forms; direct set as a fallback. if !m.goBack() { m.currentStep = StepReview } } else if m.finishedFile && m.configFileError == nil { m.currentStep = StepRunDocker m.finishedDockerRun = false m.dockerRunError = nil 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 { switch msg.String() { case "r": m.finishedDockerRun = false m.dockerRunError = nil return m, RunAppContainer( imageName, "app-dono-cliente", configPath, fmt.Sprintf("/app/%s", configPath), m.configValues, ) default: return m, tea.Quit } } else if m.finishedDockerRun && m.dockerRunError == nil { m.currentStep = StepRunUpdater m.finishedDockerRun = false m.dockerRunError = nil return m, RunUpdaterContainer( imageName, "app-dono-cliente", configPath, fmt.Sprintf("/app/%s", configPath), m.configValues, ) } } return m, nil } // updateRunUpdater starts the auto-update agent that watches the app-dono-cliente // container and pulls/recreates it whenever a new image is pushed to :latest. func (m Model) updateRunUpdater(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 { switch msg.String() { case "r": m.finishedDockerRun = false m.dockerRunError = nil return m, RunUpdaterContainer( imageName, "app-dono-cliente", configPath, fmt.Sprintf("/app/%s", configPath), m.configValues, ) default: return m, tea.Quit } } else if m.finishedDockerRun && m.dockerRunError == nil { m.currentStep = StepDone } } return m, nil }