package tui import ( "tailscale-vpn/internal/styles" tea "charm.land/bubbletea/v2" lipgloss "charm.land/lipgloss/v2" ) func footerHint(key, desc string) string { return styles.FooterKeyStyle.Render(key) + " " + styles.FooterDescStyle.Render(desc) } func footerSep() string { return styles.FooterSepStyle.Render(" · ") } func (m TUIInterface) subtitle() string { switch m.Page { case pageHome: return "Secure vpn" case pageSettings: return "Settings" case pageSelectServer: return "Select Server" default: return "Secure vpn" } } func (m TUIInterface) View() tea.View { if m.Quitting { return tea.NewView("") } w := m.WindowWidth h := m.WindowHeight if w == 0 { w = 80 } if h == 0 { h = 24 } var body string switch m.Page { case pageHome: body = m.viewHomePage() case pageSettings: body = m.viewSettingsPage() case pageSelectServer: body = m.viewSelectServerPage() default: body = m.viewHomePage() } header := lipgloss.JoinVertical(lipgloss.Left, styles.CardTitleStyle.Render("✦ tailscale vpn"), styles.CardSubtitleStyle.Render(m.subtitle()), ) topContent := styles.CardInnerStyle.Render( lipgloss.JoinVertical(lipgloss.Left, header, body), ) var footerStr string switch m.Page { case pageHome: footerStr = footerHint("↑↓", "navigate") + footerSep() + footerHint("enter", "select") + footerSep() + footerHint("r", "refresh") + footerSep() + footerHint("esc", "quit") case pageSettings: if m.SettingsEditMode == "" { if m.SettingsConfirmDelete { footerStr = footerHint("enter", "confirm") + footerSep() + footerHint("esc", "cancel") } else { footerStr = footerHint("↑↓", "navigate") + footerSep() + footerHint("enter", "select") + footerSep() + footerHint("a", "add") + footerSep() + footerHint("d", "delete") + footerSep() + footerHint("esc", "back") } } else { footerStr = footerHint("↑↓", "navigate") + footerSep() + footerHint("enter", "select") + footerSep() + footerHint("esc", "cancel") } case pageSelectServer: footerStr = footerHint("↑↓", "navigate") + footerSep() + footerHint("enter", "select") + footerSep() + footerHint("esc", "cancel") default: footerStr = footerHint("↑↓", "navigate") + footerSep() + footerHint("enter", "select") + footerSep() + footerHint("esc", "quit") } footer := styles.FooterStyle.Render(footerStr) card := styles.CardStyle.Render( lipgloss.JoinVertical(lipgloss.Left, topContent, footer), ) cardHeight := lipgloss.Height(card) topPad := max((h-cardHeight)/2, 0) centeredCard := lipgloss.NewStyle(). Width(w). Align(lipgloss.Center). PaddingTop(topPad). Render(card) v := tea.NewView(centeredCard) v.AltScreen = true return v } func (m TUIInterface) viewHomePage() string { var status string if m.VPNLoading { status = styles.VPNStatusLoadingStyle.Render("Checking...") } else if m.VPNConnected { status = styles.VPNStatusConnectedStyle.Render("● VPN Connected") } else { status = styles.VPNStatusDisconnectedStyle.Render("○ VPN Disconnected") } var serverStatus string if m.HasSelectedServer { serverStatus = styles.ServerSelectedStyle.Render("Server: " + m.SelectedServer.Name) } else if m.HasServers { serverStatus = styles.ServerNotSelectedStyle.Render("No server selected") } var menu string if !m.VPNLoading && !m.VPNToggleLoading { menu = m.renderMenu() } else { menu = styles.ButtonLoadingStyle.Render("...") } var errorStr string if m.VPNError != nil { errorStr = styles.VPNErrorStyle.Render("✗ " + m.VPNError.Error()) } content := lipgloss.JoinVertical(lipgloss.Left, status, serverStatus, menu, errorStr) return content } func (m TUIInterface) renderMenu() string { if len(m.MenuItems) == 0 { return "" } var rows []string for i, item := range m.MenuItems { if i == m.Selected { prefix := "▸ " rows = append(rows, styles.ButtonActiveStyle.Render(prefix+item.Label)) } else { prefix := " " rows = append(rows, styles.ButtonInactiveStyle.Render(prefix+item.Label)) } } return lipgloss.JoinVertical(lipgloss.Left, rows...) } func (m TUIInterface) viewSettingsPage() string { if m.SettingsEditMode != "" { return m.renderSettingsForm() } if m.SettingsConfirmDelete { return m.renderSettingsDeleteConfirm() } return m.renderSettingsList() } func (m TUIInterface) renderSettingsList() string { if len(m.SettingsServers) == 0 { return styles.EmptyListStyle.Render("No servers configured") } var rows []string for i, srv := range m.SettingsServers { if i == m.SettingsSelected { name := styles.ServerItemActiveStyle.Render(srv.Name) host := styles.ServerItemActiveStyle.Render(srv.Host) rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, "▸ ", name, styles.ServerItemInactiveStyle.Render(" · "), host)) } else { name := styles.ServerItemInactiveStyle.Render(srv.Name) host := styles.ServerItemInactiveStyle.Render(srv.Host) rows = append(rows, lipgloss.JoinHorizontal(lipgloss.Top, " ", name, styles.ServerItemInactiveStyle.Render(" · "), host)) } } return styles.ServerListStyle.Render(lipgloss.JoinVertical(lipgloss.Left, rows...)) } func (m TUIInterface) renderSettingsForm() string { title := styles.FormTitleStyle.Render( func() string { if m.SettingsEditMode == "add" { return "Add Server" } return "Edit Server" }(), ) nameLabel := styles.FormLabelStyle.Render("Name:") nameInput := func() string { if m.SettingsFormField == 0 { return styles.FormInputActiveStyle.Render(m.SettingsFormName + "_") } return styles.FormInputInactiveStyle.Render(m.SettingsFormName) }() hostLabel := styles.FormLabelStyle.Render("Host:") hostInput := func() string { if m.SettingsFormField == 1 { return styles.FormInputActiveStyle.Render(m.SettingsFormHost + "_") } return styles.FormInputInactiveStyle.Render(m.SettingsFormHost) }() saveBtn := func() string { if m.SettingsFormField == 2 { return styles.FormButtonActiveStyle.Render("Save") } return styles.FormButtonInactiveStyle.Render("Save") }() cancelBtn := func() string { if m.SettingsFormField == 3 { return styles.FormButtonActiveStyle.Render("Cancel") } return styles.FormButtonInactiveStyle.Render("Cancel") }() buttons := lipgloss.JoinHorizontal(lipgloss.Left, saveBtn, " ", cancelBtn) content := lipgloss.JoinVertical( lipgloss.Left, title, "", lipgloss.JoinHorizontal(lipgloss.Top, nameLabel, " ", nameInput), lipgloss.JoinHorizontal(lipgloss.Top, hostLabel, " ", hostInput), "", buttons, ) return styles.CardInnerStyle.Render(content) } func (m TUIInterface) renderSettingsDeleteConfirm() string { if m.SettingsSelected < 0 || m.SettingsSelected >= len(m.SettingsServers) { return "" } srv := m.SettingsServers[m.SettingsSelected] title := styles.ConfirmTitleStyle.Render("Delete Server?") message := styles.ConfirmStyle.Render("Delete \"" + srv.Name + "\"?") content := lipgloss.JoinVertical( lipgloss.Left, title, "", message, ) return styles.CardInnerStyle.Render(content) } func (m TUIInterface) viewSelectServerPage() string { return m.renderSelectServerList() } func (m TUIInterface) renderSelectServerList() string { if len(m.SelectServerServers) == 0 { return styles.EmptyListStyle.Render("No servers available") } var rows []string for i, srv := range m.SelectServerServers { isSelected := m.HasSelectedServer && srv.ID == m.SelectedServer.ID if i == m.SelectServerSelected { prefix := "▸ " row := prefix + srv.Name if isSelected { row += " " + styles.SelectedMarkerStyle.Render("✓") } rows = append(rows, styles.SelectItemActiveStyle.Render(row)) } else { prefix := " " row := prefix + srv.Name if isSelected { row += " " + styles.SelectedMarkerStyle.Render("✓") } rows = append(rows, styles.SelectItemInactiveStyle.Render(row)) } } return styles.SelectListStyle.Render(lipgloss.JoinVertical(lipgloss.Left, rows...)) }