feat: main flow created
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
tea "charm.land/bubbletea/v2"
|
tea "charm.land/bubbletea/v2"
|
||||||
"git.davinti.com.br/davinTI/app-dono/tui/tui"
|
"git.davinti.com.br/davinTI/app-dono/tui/internal/tui"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
charm.land/bubbles/v2 v2.0.0
|
charm.land/bubbles/v2 v2.0.0
|
||||||
charm.land/bubbletea/v2 v2.0.1
|
charm.land/bubbletea/v2 v2.0.1
|
||||||
|
charm.land/lipgloss/v2 v2.0.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
charm.land/lipgloss/v2 v2.0.0 // indirect
|
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||||
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -5,8 +5,14 @@ type step int
|
|||||||
const (
|
const (
|
||||||
StepCheckDocker step = iota
|
StepCheckDocker step = iota
|
||||||
StepDockerInstall
|
StepDockerInstall
|
||||||
|
StepDownloadImage
|
||||||
|
|
||||||
StepIPQuestion
|
StepIPQuestion
|
||||||
StepInstallWireguard
|
StepInstallWireguard
|
||||||
StepTextInputs
|
|
||||||
|
StepServerConfig
|
||||||
|
StepDatabaseConfig
|
||||||
|
StepCertConfig
|
||||||
|
|
||||||
StepDone
|
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()
|
||||||
|
}
|
||||||
-28
@@ -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
|
|
||||||
}
|
|
||||||
-65
@@ -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?"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user