325 lines
7.9 KiB
Go
325 lines
7.9 KiB
Go
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...))
|
|
}
|