feat: main flow created
This commit is contained in:
@@ -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,18 @@
|
||||
package tui
|
||||
|
||||
type step int
|
||||
|
||||
const (
|
||||
StepCheckDocker step = iota
|
||||
StepDockerInstall
|
||||
StepDownloadImage
|
||||
|
||||
StepIPQuestion
|
||||
StepInstallWireguard
|
||||
|
||||
StepServerConfig
|
||||
StepDatabaseConfig
|
||||
StepCertConfig
|
||||
|
||||
StepDone
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user