init
This commit is contained in:
324
internal/tui/view.go
Normal file
324
internal/tui/view.go
Normal file
@@ -0,0 +1,324 @@
|
||||
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...))
|
||||
}
|
||||
Reference in New Issue
Block a user