Browse Source

feat: implemented all features besides wireguard

master
jb 3 weeks ago
parent
commit
085cda7251
11 changed files with 539 additions and 73 deletions
  1. +1
    -0
      .gitignore
  2. +0
    -27
      internal/docker/docker.go
  3. +57
    -6
      internal/tui/cmds.go
  4. +75
    -0
      internal/tui/config.go
  5. +96
    -0
      internal/tui/docker.go
  6. +10
    -9
      internal/tui/form.go
  7. +70
    -11
      internal/tui/model.go
  8. +9
    -0
      internal/tui/steps.go
  9. +3
    -0
      internal/tui/styles.go
  10. +97
    -14
      internal/tui/update.go
  11. +121
    -6
      internal/tui/view.go

+ 1
- 0
.gitignore View File

@ -0,0 +1 @@
config.toml

+ 0
- 27
internal/docker/docker.go View File

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

+ 57
- 6
internal/tui/cmds.go View File

@ -1,6 +1,7 @@
package tui package tui
import ( import (
"fmt"
"os/exec" "os/exec"
"time" "time"
@ -14,11 +15,18 @@ type DockerCheckedMsg struct{ Installed bool }
type DockerInstalledMsg struct{ Err error } type DockerInstalledMsg struct{ Err error }
type TickMsg struct{} type TickMsg struct{}
// Image downloading
// Image downloading and checking
type ImageCheckMsg struct {
Exists bool
Err error
}
type ImageDownloadFinishedMsg struct { type ImageDownloadFinishedMsg struct {
Err error
Message string
Err error
} }
type DownloadTickMsg struct{} type DownloadTickMsg struct{}
type ConfigFileMsg struct{ Err error }
type DockerRunMsg struct{ Err error }
// --- Commands --- // --- 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 tea.Tick(100*time.Millisecond, func(t time.Time) tea.Msg {
return TickMsg{} return TickMsg{}
}) })
} }
func downloadImageCmd() tea.Cmd {
func DownloadImageCmd(username, password string) tea.Cmd {
return func() tea.Msg { 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, Err: err,
} }
} }


+ 75
- 0
internal/tui/config.go View File

@ -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)
}

+ 96
- 0
internal/tui/docker.go View File

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

+ 10
- 9
internal/tui/form.go View File

@ -18,12 +18,13 @@ const (
) )
type FormField struct { type FormField struct {
Id string
Label string Label string
Placeholder string Placeholder string
Default string Default string
Type FieldType Type FieldType
Options []string // for FieldTypeSelect Options []string // for FieldTypeSelect
optionIdx int // for FieldTypeSelect
OptionIdx int // for FieldTypeSelect
CharLimit int CharLimit int
input textinput.Model input textinput.Model
} }
@ -41,7 +42,7 @@ func NewFormStep(title string, fields []FormField) FormStep {
for j, opt := range f.Fields[i].Options { for j, opt := range f.Fields[i].Options {
// Apply default option // Apply default option
if opt == f.Fields[i].Default { if opt == f.Fields[i].Default {
f.Fields[i].optionIdx = j
f.Fields[i].OptionIdx = j
break break
} }
} }
@ -81,13 +82,13 @@ func (f *FormStep) Update(msg tea.Msg) (done bool, cmd tea.Cmd) {
switch msg.String() { switch msg.String() {
// Select specific inputs // Select specific inputs
case "left", "h": case "left", "h":
if isSelect && focused.optionIdx > 0 {
focused.optionIdx--
if isSelect && focused.OptionIdx > 0 {
focused.OptionIdx--
} }
case "right", "l": 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 // Generic inputs
@ -120,9 +121,9 @@ func (f *FormStep) Values() map[string]string {
out := make(map[string]string) out := make(map[string]string)
for _, field := range f.Fields { for _, field := range f.Fields {
if field.Type == FieldTypeSelect { if field.Type == FieldTypeSelect {
out[field.Label] = field.Options[field.optionIdx]
out[field.Id] = field.Options[field.OptionIdx]
} else { } else {
out[field.Label] = field.input.Value()
out[field.Id] = field.input.Value()
} }
} }
return out return out
@ -155,7 +156,7 @@ func renderField(field FormField, focused bool) string {
case FieldTypeSelect: case FieldTypeSelect:
var opts []string var opts []string
for i, opt := range field.Options { for i, opt := range field.Options {
if i == field.optionIdx {
if i == field.OptionIdx {
opts = append(opts, SelectedStyle.Render("[ "+opt+" ]")) opts = append(opts, SelectedStyle.Render("[ "+opt+" ]"))
} else { } else {
opts = append(opts, DimStyle.Render(" "+opt+" ")) opts = append(opts, DimStyle.Render(" "+opt+" "))


+ 70
- 11
internal/tui/model.go View File

@ -1,37 +1,78 @@
package tui package tui
import ( import (
"charm.land/bubbles/v2/spinner"
tea "charm.land/bubbletea/v2" tea "charm.land/bubbletea/v2"
) )
type Model struct { type Model struct {
currentStep step currentStep step
cursor int cursor int
width int
height int
spinner spinner.Model
loading bool
dockerInstalled bool dockerInstalled bool
checkDockerDone bool checkDockerDone bool
checkProgress float64 checkProgress float64
isPublicIP bool
downloadDone bool
downloadMessage string
downloadError error
loginData DockerLoginData
isPublicIP bool
loginForm FormStep
serverForm FormStep serverForm FormStep
dbForm FormStep dbForm FormStep
certForm FormStep certForm FormStep
configValues ConfigValues configValues ConfigValues
finishedFile bool
configFileError error
finishedDockerRun bool
dockerRunError error
err error err error
} }
type DockerLoginData struct {
Login string
Password string
}
type ConfigValues struct { type ConfigValues struct {
Login map[string]string
Server map[string]string Server map[string]string
Database map[string]string Database map[string]string
Cert map[string]string Cert map[string]string
} }
func InitialModel() Model { func InitialModel() Model {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = SpinnerStyle
return Model{ return Model{
currentStep: StepCheckDocker, 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{ serverForm: NewFormStep("Servidor", []FormField{
{ {
Id: "port",
Label: "Porta", Label: "Porta",
Placeholder: "8081", Placeholder: "8081",
Default: "8081", Default: "8081",
@ -39,6 +80,7 @@ func InitialModel() Model {
CharLimit: 4, CharLimit: 4,
}, },
{ {
Id: "timeout",
Label: "Timeout (Segundos)", Label: "Timeout (Segundos)",
Placeholder: "30", Placeholder: "30",
Default: "30", Default: "30",
@ -46,6 +88,7 @@ func InitialModel() Model {
CharLimit: 3, CharLimit: 3,
}, },
{ {
Id: "environment",
Label: "Ambiente", Label: "Ambiente",
Default: "production", Default: "production",
Type: FieldTypeSelect, Type: FieldTypeSelect,
@ -54,24 +97,28 @@ func InitialModel() Model {
}), }),
dbForm: NewFormStep("Banco de Dados", []FormField{ dbForm: NewFormStep("Banco de Dados", []FormField{
{ {
Id: "database_type",
Label: "Tipo do Banco", Label: "Tipo do Banco",
Default: "postgres", Default: "postgres",
Type: FieldTypeSelect, Type: FieldTypeSelect,
Options: []string{"postgres", "oracle"}, Options: []string{"postgres", "oracle"},
}, },
{ {
Id: "database_url",
Label: "URL de acesso", Label: "URL de acesso",
Placeholder: "postgres://usuario:senha@banco:5432/app_dono_db", Placeholder: "postgres://usuario:senha@banco:5432/app_dono_db",
Default: "postgres://usuario:senha@banco:5432/app_dono_db", Default: "postgres://usuario:senha@banco:5432/app_dono_db",
Type: FieldTypeText, Type: FieldTypeText,
}, },
{ {
Id: "max_conns",
Label: "Conexões ativas (máximo)", Label: "Conexões ativas (máximo)",
Placeholder: "10", Placeholder: "10",
Default: "10", Default: "10",
Type: FieldTypeNumber, Type: FieldTypeNumber,
}, },
{ {
Id: "min_conns",
Label: "Conexões ativas (mínimo)", Label: "Conexões ativas (mínimo)",
Placeholder: "2", Placeholder: "2",
Default: "2", Default: "2",
@ -80,33 +127,45 @@ func InitialModel() Model {
}), }),
certForm: NewFormStep("Certificado", []FormField{ 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, 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, 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, Type: FieldTypeText,
}, },
{ {
Id: "server_name",
Label: "Nome do servidor", Label: "Nome do servidor",
Placeholder: "client", Placeholder: "client",
Default: "client", Default: "client",
Type: FieldTypeText, Type: FieldTypeText,
}, },
}), }),
spinner: s,
} }
} }
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
return tea.Batch(CheckDockerCmd(), tickCmd())
return tea.Batch(CheckDockerCmd(), TickCmd(), m.spinner.Tick)
} }

+ 9
- 0
internal/tui/steps.go View File

@ -3,16 +3,25 @@ package tui
type step int type step int
const ( const (
// Docker Validation
StepCheckDocker step = iota StepCheckDocker step = iota
StepDockerInstall StepDockerInstall
// Image Fetching
StepDockerLogin
StepDownloadImage StepDownloadImage
// IP Stuff
StepIPQuestion StepIPQuestion
StepInstallWireguard StepInstallWireguard
// Docker Config
StepServerConfig StepServerConfig
StepDatabaseConfig StepDatabaseConfig
StepCertConfig StepCertConfig
// Finalizing
StepGenerateFile
StepRunDocker
StepDone StepDone
) )

+ 3
- 0
internal/tui/styles.go View File

@ -53,4 +53,7 @@ var (
Foreground(lipgloss.Color(secondary)). Foreground(lipgloss.Color(secondary)).
Bold(true). Bold(true).
Border(lipgloss.NormalBorder(), false, false, true) Border(lipgloss.NormalBorder(), false, false, true)
SpinnerStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("205"))
) )

+ 97
- 14
internal/tui/update.go View File

@ -1,11 +1,18 @@
package tui package tui
import ( import (
"fmt"
"math/rand" "math/rand"
"charm.land/bubbles/v2/spinner"
tea "charm.land/bubbletea/v2" 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) { func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if key, ok := msg.(tea.KeyPressMsg); ok { if key, ok := msg.(tea.KeyPressMsg); ok {
switch key.String() { 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 { switch m.currentStep {
case StepCheckDocker: case StepCheckDocker:
return m.updateCheckDocker(msg) return m.updateCheckDocker(msg)
case StepDockerInstall: case StepDockerInstall:
return m.updateDockerInstall(msg) 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: case StepDownloadImage:
return m.updateDownloadImage(msg) return m.updateDownloadImage(msg)
case StepIPQuestion: case StepIPQuestion:
@ -29,6 +57,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
done, cmd := m.serverForm.Update(msg) done, cmd := m.serverForm.Update(msg)
if done { if done {
m.configValues.Server = m.serverForm.Values()
m.currentStep = StepDatabaseConfig m.currentStep = StepDatabaseConfig
} }
@ -37,6 +66,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
done, cmd := m.dbForm.Update(msg) done, cmd := m.dbForm.Update(msg)
if done { if done {
m.configValues.Database = m.dbForm.Values()
m.currentStep = StepCertConfig m.currentStep = StepCertConfig
} }
@ -45,10 +75,17 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
done, cmd := m.certForm.Update(msg) done, cmd := m.certForm.Update(msg)
if done { if done {
m.currentStep = StepDone
m.configValues.Cert = m.certForm.Values()
m.currentStep = StepGenerateFile
return m, GenerateConfigFile(m.configValues, configPath)
} }
return m, cmd return m, cmd
case StepGenerateFile:
return m.updateGenerateFile(msg)
case StepRunDocker:
return m.updateRunDocker(msg)
case StepDone: case StepDone:
return m, tea.Quit return m, tea.Quit
} }
@ -61,19 +98,19 @@ func (m Model) updateCheckDocker(msg tea.Msg) (tea.Model, tea.Cmd) {
case DockerCheckedMsg: case DockerCheckedMsg:
m.dockerInstalled = msg.Installed m.dockerInstalled = msg.Installed
m.checkDockerDone = true m.checkDockerDone = true
case TickMsg: case TickMsg:
if m.checkProgress < 1.0 { 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 { if m.checkProgress > 1.0 {
m.checkProgress = 1.0 m.checkProgress = 1.0
} }
return m, tickCmd()
return m, TickCmd()
} }
case tea.KeyPressMsg: case tea.KeyPressMsg:
if m.checkDockerDone {
if m.checkDockerDone && m.checkProgress == 1 {
if m.dockerInstalled { if m.dockerInstalled {
m.currentStep = StepIPQuestion
m.loading = true
m.currentStep = StepDockerLogin
} else { } else {
m.currentStep = StepDockerInstall 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) { func (m Model) updateDockerInstall(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
switch msg.(type) {
case tea.KeyPressMsg: case tea.KeyPressMsg:
return m, tea.Quit 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 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 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: 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 return m, nil


+ 121
- 6
internal/tui/view.go View File

@ -5,34 +5,63 @@ import (
"strings" "strings"
tea "charm.land/bubbletea/v2" tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
) )
const ( 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 { func (m Model) View() tea.View {
pad := strings.Repeat(" ", padding) pad := strings.Repeat(" ", padding)
var body string var body string
helpMsg := "ctrl+c: sair"
helpMsg := defaultMsg
switch m.currentStep { switch m.currentStep {
// Docker stuff
case StepCheckDocker: case StepCheckDocker:
body = m.viewCheckDocker() body = m.viewCheckDocker()
case StepDockerInstall: case StepDockerInstall:
body = m.viewDockerInstall() 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: case StepIPQuestion:
body = m.viewIPQuestion() body = m.viewIPQuestion()
// App Config Stuff
case StepServerConfig: case StepServerConfig:
body = m.serverForm.View() body = m.serverForm.View()
helpMsg = "tab: próximo campo • enter: confirmar • ctrl+c: sair"
helpMsg = formMsg
case StepDatabaseConfig: case StepDatabaseConfig:
body = m.dbForm.View() body = m.dbForm.View()
helpMsg = "tab: próximo campo • enter: confirmar • ctrl+c: sair"
helpMsg = formMsg
case StepCertConfig: case StepCertConfig:
body = m.certForm.View() 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) help := HelpStyle.Render(helpMsg)
@ -88,6 +117,32 @@ func (m Model) viewDockerInstall() string {
return sb.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 { func (m Model) viewIPQuestion() string {
pad := strings.Repeat(" ", padding) pad := strings.Repeat(" ", padding)
@ -113,3 +168,63 @@ func (m Model) viewIPQuestion() string {
return sb.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()
}

Loading…
Cancel
Save