// Package ui provides the bubbletea terminal UI for wallarm: // - Deploy choice: local deploy or remote assistance // - Dashboard: lists existing nodes with actions // // Deployment logic runs in plain terminal (main.go), not inside the TUI. package ui import ( "fmt" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "git.sechpoint.app/customer-engineering/wallarm/internal/state" ) var ( titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")).MarginBottom(1) goodStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")) warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("227")) badStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")) dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) activeStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("63")).Bold(true) ) type Model struct { state stateView width int height int } type stateView int const ( viewRoute stateView = iota viewDeployChoice // local deploy or remote assist? viewDashboard ) // Run starts the bubbletea TUI. Returns nil if user chose local deploy (exit and run form). func Run() error { m := Model{state: viewRoute} p := tea.NewProgram(m) if _, err := p.Run(); err != nil { return err } return nil } func (m Model) Init() tea.Cmd { return nil } func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "q", "ctrl+c": return m, tea.Quit case "1": if m.state == viewDeployChoice { return m, tea.Quit // exit TUI, main.go runs deploy form } case "2": if m.state == viewDeployChoice { return m, tea.Quit // exit TUI, main.go runs tunnel flow } } case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height } switch m.state { case viewRoute: if state.HasDeployment() { m.state = viewDashboard } else { m.state = viewDeployChoice } } return m, nil } func (m Model) View() string { switch m.state { case viewRoute: return "" case viewDeployChoice: return deployChoiceView(m) case viewDashboard: return dashboardView(m) } return "" } // ─── Deploy Choice ─────────────────────────────────────────────────── func deployChoiceView(m Model) string { s := titleStyle.Render("🛡️ Wallarm Setup") + "\n\n" s += "How would you like to proceed?\n\n" s += activeStyle.Render(" [1] Deploy locally") + "\n" s += " Configure and deploy the Wallarm node on this server now.\n\n" s += activeStyle.Render(" [2] Remote assistance") + "\n" s += " Open a secure tunnel so a remote admin can deploy for you.\n\n" s += dimStyle.Render("Press 1 or 2, q to quit") return s } // ─── Dashboard ─────────────────────────────────────────────────────── func dashboardView(m Model) string { s, err := state.Load() if err != nil || s == nil { return badStyle.Render("Error loading state.") + "\nPress q to quit" } out := titleStyle.Render("📊 Wallarm Dashboard") + "\n" out += fmt.Sprintf("Type: %s | Cloud: %s (%s)\n", s.DeploymentType, s.CloudRegion, s.APIHost) out += dimStyle.Render(strings.Repeat("─", 50)) + "\n\n" if len(s.Nodes) == 0 { out += dimStyle.Render("No nodes deployed yet.") + "\n\n" } else { out += activeStyle.Render("Nodes:") + "\n" for _, n := range s.Nodes { marker := "●" style := goodStyle if n.Status != "running" { marker = "○" style = warnStyle } out += style.Render(fmt.Sprintf(" %s %s — %s", marker, n.Name, n.Status)) if n.Address != "" { out += dimStyle.Render(fmt.Sprintf(" (%s)", n.Address)) } out += "\n" } out += "\n" } out += "Actions:\n" out += fmt.Sprintf(" %s\n", activeStyle.Render("[a] Add node")) out += fmt.Sprintf(" %s\n", activeStyle.Render("[c] Configure")) out += fmt.Sprintf(" %s\n", activeStyle.Render("[r] Remove node")) out += fmt.Sprintf(" %s\n", activeStyle.Render("[t] Start tunnel")) out += fmt.Sprintf(" %s\n", dimStyle.Render("[q] Quit")) return out }