| @ -0,0 +1,17 @@ | |||||
| package main | |||||
| import ( | |||||
| "fmt" | |||||
| "os" | |||||
| tea "charm.land/bubbletea/v2" | |||||
| "git.davinti.com.br/davinTI/app-dono/tui/tui" | |||||
| ) | |||||
| func main() { | |||||
| p := tea.NewProgram(tui.InitialModel()) | |||||
| if _, err := p.Run(); err != nil { | |||||
| fmt.Fprintf(os.Stderr, "error: %v\n", err) | |||||
| os.Exit(1) | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,28 @@ | |||||
| module git.davinti.com.br/davinTI/app-dono/tui | |||||
| go 1.25.0 | |||||
| require ( | |||||
| charm.land/bubbles/v2 v2.0.0 | |||||
| charm.land/bubbletea/v2 v2.0.1 | |||||
| ) | |||||
| require ( | |||||
| charm.land/lipgloss/v2 v2.0.0 // indirect | |||||
| github.com/atotto/clipboard v0.1.4 // indirect | |||||
| github.com/charmbracelet/colorprofile v0.4.2 // indirect | |||||
| github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect | |||||
| github.com/charmbracelet/x/ansi v0.11.6 // indirect | |||||
| github.com/charmbracelet/x/term v0.2.2 // indirect | |||||
| github.com/charmbracelet/x/termios v0.1.1 // indirect | |||||
| github.com/charmbracelet/x/windows v0.2.2 // indirect | |||||
| github.com/clipperhouse/displaywidth v0.11.0 // indirect | |||||
| github.com/clipperhouse/uax29/v2 v2.7.0 // indirect | |||||
| github.com/lucasb-eyer/go-colorful v1.3.0 // indirect | |||||
| github.com/mattn/go-runewidth v0.0.20 // indirect | |||||
| github.com/muesli/cancelreader v0.2.2 // indirect | |||||
| github.com/rivo/uniseg v0.4.7 // indirect | |||||
| github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect | |||||
| golang.org/x/sync v0.19.0 // indirect | |||||
| golang.org/x/sys v0.41.0 // indirect | |||||
| ) | |||||
| @ -0,0 +1,44 @@ | |||||
| charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= | |||||
| charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= | |||||
| charm.land/bubbletea/v2 v2.0.1 h1:B8e9zzK7x9JJ+XvHGF4xnYu9Xa0E0y0MyggY6dbaCfQ= | |||||
| charm.land/bubbletea/v2 v2.0.1/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= | |||||
| charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo= | |||||
| charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14= | |||||
| github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= | |||||
| github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= | |||||
| github.com/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM= | |||||
| github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= | |||||
| github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= | |||||
| github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= | |||||
| github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= | |||||
| github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= | |||||
| github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= | |||||
| github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= | |||||
| github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= | |||||
| github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= | |||||
| github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= | |||||
| github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= | |||||
| github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= | |||||
| github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= | |||||
| github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= | |||||
| github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= | |||||
| github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= | |||||
| github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= | |||||
| github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= | |||||
| github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= | |||||
| github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= | |||||
| github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | |||||
| github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= | |||||
| github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= | |||||
| github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= | |||||
| github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= | |||||
| github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= | |||||
| github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | |||||
| github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= | |||||
| github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= | |||||
| golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= | |||||
| golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= | |||||
| golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= | |||||
| golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= | |||||
| golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= | |||||
| golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= | |||||
| @ -0,0 +1,28 @@ | |||||
| 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} | |||||
| } | |||||
| } | |||||
| @ -0,0 +1 @@ | |||||
| package tui | |||||
| @ -0,0 +1,25 @@ | |||||
| 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() | |||||
| } | |||||
| @ -0,0 +1,12 @@ | |||||
| package tui | |||||
| type step int | |||||
| const ( | |||||
| StepCheckDocker step = iota | |||||
| StepDockerInstall | |||||
| StepIPQuestion | |||||
| StepInstallWireguard | |||||
| StepTextInputs | |||||
| StepDone | |||||
| ) | |||||
| @ -0,0 +1,12 @@ | |||||
| 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")) | |||||
| ) | |||||
| @ -0,0 +1,76 @@ | |||||
| 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 | |||||
| } | |||||
| @ -0,0 +1,65 @@ | |||||
| 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?" | |||||
| } | |||||