init
This commit is contained in:
100
internal/tui/commands.go
Normal file
100
internal/tui/commands.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"tailscale-vpn/internal/pages"
|
||||
"tailscale-vpn/internal/services"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
func checkVPNStatusCmd(store *services.ServicesStore) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
connected, err := store.VPN.CheckStatus(ctx)
|
||||
return pages.VPNStatusMsg{
|
||||
Connected: connected,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func turnOnVPNCmd(store *services.ServicesStore) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
if err := store.VPN.TurnOn(ctx); err != nil {
|
||||
return pages.VPNToggleMsg{
|
||||
Connected: false,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
connected, _ := store.VPN.CheckStatus(ctx)
|
||||
return pages.VPNToggleMsg{
|
||||
Connected: connected,
|
||||
Err: nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func turnOffVPNCmd(store *services.ServicesStore) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
ctx := context.Background()
|
||||
if err := store.VPN.TurnOff(ctx); err != nil {
|
||||
return pages.VPNToggleMsg{
|
||||
Connected: true,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
connected, _ := store.VPN.CheckStatus(ctx)
|
||||
return pages.VPNToggleMsg{
|
||||
Connected: connected,
|
||||
Err: nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadServersCmd(store *services.ServicesStore) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
servers := store.Config.GetServers()
|
||||
return pages.ServerListLoadedMsg{
|
||||
Servers: servers,
|
||||
Err: nil,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addServerCmd(store *services.ServicesStore, name, host string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := store.Config.AddServer(name, host)
|
||||
return pages.ServerSaveMsg{Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func updateServerCmd(store *services.ServicesStore, id, name, host string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := store.Config.UpdateServer(id, name, host)
|
||||
return pages.ServerSaveMsg{Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteServerCmd(store *services.ServicesStore, id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := store.Config.RemoveServer(id)
|
||||
return pages.ServerDeleteMsg{Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func selectServerCmd(store *services.ServicesStore, serverID string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := store.Config.SetSelectedServer(serverID)
|
||||
if err != nil {
|
||||
return pages.ServerSelectedMsg{Err: err}
|
||||
}
|
||||
server, ok := store.Config.GetSelectedServer()
|
||||
if !ok {
|
||||
return pages.ServerSelectedMsg{Err: fmt.Errorf("failed to get selected server")}
|
||||
}
|
||||
return pages.ServerSelectedMsg{Server: server, Err: nil}
|
||||
}
|
||||
}
|
||||
15
internal/tui/init.go
Normal file
15
internal/tui/init.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"tailscale-vpn/internal/pages"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
func (m TUIInterface) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
func() tea.Msg { return pages.HomePageMsg{} },
|
||||
checkVPNStatusCmd(m.Services),
|
||||
loadServersCmd(m.Services),
|
||||
)
|
||||
}
|
||||
56
internal/tui/tui.go
Normal file
56
internal/tui/tui.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"tailscale-vpn/internal/pages"
|
||||
"tailscale-vpn/internal/services"
|
||||
)
|
||||
|
||||
type page int
|
||||
|
||||
const (
|
||||
pageHome page = iota
|
||||
pageSettings
|
||||
pageSelectServer
|
||||
)
|
||||
|
||||
type TUIInterface struct {
|
||||
Services *services.ServicesStore
|
||||
Page page
|
||||
MenuItems []pages.MenuItem
|
||||
Selected int
|
||||
Quitting bool
|
||||
WindowWidth int
|
||||
WindowHeight int
|
||||
VPNConnected bool
|
||||
VPNLoading bool
|
||||
VPNToggleLoading bool
|
||||
VPNError error
|
||||
HasServers bool
|
||||
SelectedServer services.Server
|
||||
HasSelectedServer bool
|
||||
SettingsServers []services.Server
|
||||
SettingsSelected int
|
||||
SettingsEditMode string
|
||||
SettingsEditingServer services.Server
|
||||
SettingsFormName string
|
||||
SettingsFormHost string
|
||||
SettingsFormField int
|
||||
SettingsConfirmDelete bool
|
||||
SelectServerServers []services.Server
|
||||
SelectServerSelected int
|
||||
}
|
||||
|
||||
func NewTUIInterface(store *services.ServicesStore) TUIInterface {
|
||||
servers := store.Config.GetServers()
|
||||
selectedServer, hasSelected := store.Config.GetSelectedServer()
|
||||
return TUIInterface{
|
||||
Services: store,
|
||||
Page: pageHome,
|
||||
MenuItems: pages.HomeMenuItems(false, len(servers) > 0),
|
||||
VPNConnected: false,
|
||||
VPNLoading: true,
|
||||
HasServers: len(servers) > 0,
|
||||
SelectedServer: selectedServer,
|
||||
HasSelectedServer: hasSelected,
|
||||
}
|
||||
}
|
||||
344
internal/tui/update.go
Normal file
344
internal/tui/update.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"tailscale-vpn/internal/pages"
|
||||
"tailscale-vpn/internal/services"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
)
|
||||
|
||||
func (m TUIInterface) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case pages.HomePageMsg:
|
||||
m.Page = pageHome
|
||||
m.MenuItems = pages.HomeMenuItems(m.VPNConnected, m.HasServers)
|
||||
return m, nil
|
||||
|
||||
case pages.SettingsPageMsg:
|
||||
m.Page = pageSettings
|
||||
m.SettingsEditMode = ""
|
||||
m.SettingsConfirmDelete = false
|
||||
m.SettingsSelected = 0
|
||||
return m, loadServersCmd(m.Services)
|
||||
|
||||
case pages.SelectServerPageMsg:
|
||||
m.Page = pageSelectServer
|
||||
m.SelectServerServers = m.SettingsServers
|
||||
m.SelectServerSelected = 0
|
||||
for i, srv := range m.SelectServerServers {
|
||||
if m.HasSelectedServer && srv.ID == m.SelectedServer.ID {
|
||||
m.SelectServerSelected = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case pages.ServerSelectedMsg:
|
||||
m.SelectedServer = msg.Server
|
||||
m.HasSelectedServer = msg.Server.ID != ""
|
||||
if msg.Err != nil {
|
||||
m.VPNError = msg.Err
|
||||
}
|
||||
return m, func() tea.Msg { return pages.HomePageMsg{} }
|
||||
|
||||
case pages.ServerSelectionCanceledMsg:
|
||||
return m, func() tea.Msg { return pages.HomePageMsg{} }
|
||||
|
||||
case pages.ServerListLoadedMsg:
|
||||
m.SettingsServers = msg.Servers
|
||||
m.HasServers = len(msg.Servers) > 0
|
||||
if msg.Err != nil {
|
||||
m.VPNError = msg.Err
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case pages.ServerEditMsg:
|
||||
m.SettingsEditMode = msg.Mode
|
||||
m.SettingsConfirmDelete = false
|
||||
if msg.Mode == "edit" {
|
||||
m.SettingsEditingServer = msg.Server
|
||||
m.SettingsFormName = msg.Server.Name
|
||||
m.SettingsFormHost = msg.Server.Host
|
||||
} else {
|
||||
m.SettingsEditingServer = services.Server{}
|
||||
m.SettingsFormName = ""
|
||||
m.SettingsFormHost = ""
|
||||
}
|
||||
m.SettingsFormField = 0
|
||||
return m, nil
|
||||
|
||||
case pages.ServerSaveMsg:
|
||||
m.SettingsEditMode = ""
|
||||
if msg.Err != nil {
|
||||
m.VPNError = msg.Err
|
||||
}
|
||||
return m, loadServersCmd(m.Services)
|
||||
|
||||
case pages.ServerDeleteMsg:
|
||||
m.SettingsConfirmDelete = false
|
||||
if msg.Err != nil {
|
||||
m.VPNError = msg.Err
|
||||
} else {
|
||||
if m.SettingsSelected >= len(m.SettingsServers) {
|
||||
m.SettingsSelected = max(len(m.SettingsServers)-1, 0)
|
||||
}
|
||||
}
|
||||
return m, loadServersCmd(m.Services)
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.WindowWidth = msg.Width
|
||||
m.WindowHeight = msg.Height
|
||||
return m, nil
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
if m.Page == pageHome {
|
||||
return m.updateHomePage(msg)
|
||||
}
|
||||
if m.Page == pageSettings {
|
||||
return m.updateSettingsPage(msg)
|
||||
}
|
||||
if m.Page == pageSelectServer {
|
||||
return m.updateSelectServerPage(msg)
|
||||
}
|
||||
|
||||
case tea.PasteMsg:
|
||||
if m.Page == pageSettings && m.SettingsEditMode != "" {
|
||||
if m.SettingsFormField == 0 {
|
||||
m.SettingsFormName += msg.Content
|
||||
} else if m.SettingsFormField == 1 {
|
||||
m.SettingsFormHost += msg.Content
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
case pages.VPNStatusMsg:
|
||||
m.VPNConnected = msg.Connected
|
||||
m.VPNLoading = false
|
||||
if msg.Err != nil {
|
||||
m.VPNError = msg.Err
|
||||
} else {
|
||||
m.VPNError = nil
|
||||
}
|
||||
m.MenuItems = pages.HomeMenuItems(m.VPNConnected, m.HasServers)
|
||||
return m, nil
|
||||
|
||||
case pages.VPNToggleMsg:
|
||||
m.VPNConnected = msg.Connected
|
||||
m.VPNToggleLoading = false
|
||||
if msg.Err != nil {
|
||||
m.VPNError = msg.Err
|
||||
} else {
|
||||
m.VPNError = nil
|
||||
}
|
||||
m.MenuItems = pages.HomeMenuItems(m.VPNConnected, m.HasServers)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m TUIInterface) updateHomePage(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
if m.VPNLoading || m.VPNToggleLoading {
|
||||
if msg.String() == "ctrl+c" {
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.Selected > 0 {
|
||||
m.Selected--
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "down", "j":
|
||||
if m.Selected < len(m.MenuItems)-1 {
|
||||
m.Selected++
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "enter":
|
||||
if len(m.MenuItems) > 0 {
|
||||
switch m.MenuItems[m.Selected].Key {
|
||||
case "select-server":
|
||||
return m, func() tea.Msg { return pages.SelectServerPageMsg{} }
|
||||
case "settings":
|
||||
return m, func() tea.Msg { return pages.SettingsPageMsg{} }
|
||||
case "on":
|
||||
m.VPNToggleLoading = true
|
||||
m.VPNError = nil
|
||||
return m, turnOnVPNCmd(m.Services)
|
||||
case "off":
|
||||
m.VPNToggleLoading = true
|
||||
m.VPNError = nil
|
||||
return m, turnOffVPNCmd(m.Services)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "r":
|
||||
m.VPNLoading = true
|
||||
m.VPNError = nil
|
||||
return m, checkVPNStatusCmd(m.Services)
|
||||
|
||||
case "ctrl+c", "esc":
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m TUIInterface) updateSelectServerPage(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.SelectServerSelected > 0 {
|
||||
m.SelectServerSelected--
|
||||
}
|
||||
return m, nil
|
||||
case "down", "j":
|
||||
if m.SelectServerSelected < len(m.SelectServerServers)-1 {
|
||||
m.SelectServerSelected++
|
||||
}
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.SelectServerSelected >= 0 && m.SelectServerSelected < len(m.SelectServerServers) {
|
||||
return m, selectServerCmd(m.Services, m.SelectServerServers[m.SelectServerSelected].ID)
|
||||
}
|
||||
return m, nil
|
||||
case "esc", "ctrl+c":
|
||||
return m, func() tea.Msg { return pages.ServerSelectionCanceledMsg{} }
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m TUIInterface) updateSettingsPage(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
|
||||
if m.SettingsEditMode != "" {
|
||||
switch msg.String() {
|
||||
case "esc":
|
||||
m.SettingsEditMode = ""
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.SettingsFormField == 2 {
|
||||
if m.SettingsFormName == "" || m.SettingsFormHost == "" {
|
||||
m.VPNError = errors.New("name and host are required")
|
||||
return m, nil
|
||||
}
|
||||
if m.SettingsEditMode == "add" {
|
||||
return m, addServerCmd(m.Services, m.SettingsFormName, m.SettingsFormHost)
|
||||
}
|
||||
return m, updateServerCmd(m.Services, m.SettingsEditingServer.ID, m.SettingsFormName, m.SettingsFormHost)
|
||||
} else if m.SettingsFormField == 3 {
|
||||
m.SettingsEditMode = ""
|
||||
return m, nil
|
||||
} else {
|
||||
m.SettingsFormField = (m.SettingsFormField + 1) % 4
|
||||
return m, nil
|
||||
}
|
||||
case "up", "k":
|
||||
m.SettingsFormField = (m.SettingsFormField - 1 + 4) % 4
|
||||
return m, nil
|
||||
case "down", "j":
|
||||
m.SettingsFormField = (m.SettingsFormField + 1) % 4
|
||||
return m, nil
|
||||
case "tab":
|
||||
m.SettingsFormField = (m.SettingsFormField + 1) % 4
|
||||
return m, nil
|
||||
case "backtab":
|
||||
m.SettingsFormField = (m.SettingsFormField - 1 + 4) % 4
|
||||
return m, nil
|
||||
case "backspace":
|
||||
if m.SettingsFormField == 0 && len(m.SettingsFormName) > 0 {
|
||||
m.SettingsFormName = m.SettingsFormName[:len(m.SettingsFormName)-1]
|
||||
} else if m.SettingsFormField == 1 && len(m.SettingsFormHost) > 0 {
|
||||
m.SettingsFormHost = m.SettingsFormHost[:len(m.SettingsFormHost)-1]
|
||||
}
|
||||
return m, nil
|
||||
case "ctrl+c":
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
default:
|
||||
if m.SettingsFormField == 0 || m.SettingsFormField == 1 {
|
||||
if msg.Text != "" {
|
||||
if m.SettingsFormField == 0 {
|
||||
m.SettingsFormName += msg.Text
|
||||
} else {
|
||||
m.SettingsFormHost += msg.Text
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
switch msg.String() {
|
||||
|
||||
case "esc":
|
||||
if m.SettingsConfirmDelete {
|
||||
m.SettingsConfirmDelete = false
|
||||
return m, nil
|
||||
}
|
||||
return m, func() tea.Msg { return pages.HomePageMsg{} }
|
||||
|
||||
case "up", "k":
|
||||
if m.SettingsConfirmDelete {
|
||||
return m, nil
|
||||
}
|
||||
if m.SettingsSelected > 0 {
|
||||
m.SettingsSelected--
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "down", "j":
|
||||
if m.SettingsConfirmDelete {
|
||||
return m, nil
|
||||
}
|
||||
if m.SettingsSelected < len(m.SettingsServers)-1 {
|
||||
m.SettingsSelected++
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "a":
|
||||
if m.SettingsConfirmDelete {
|
||||
return m, nil
|
||||
}
|
||||
return m, func() tea.Msg { return pages.ServerEditMsg{Mode: "add"} }
|
||||
|
||||
case "enter":
|
||||
if m.SettingsConfirmDelete {
|
||||
if m.SettingsSelected >= 0 && m.SettingsSelected < len(m.SettingsServers) {
|
||||
return m, deleteServerCmd(m.Services, m.SettingsServers[m.SettingsSelected].ID)
|
||||
}
|
||||
m.SettingsConfirmDelete = false
|
||||
return m, nil
|
||||
}
|
||||
if m.SettingsSelected >= 0 && m.SettingsSelected < len(m.SettingsServers) {
|
||||
return m, func() tea.Msg {
|
||||
return pages.ServerEditMsg{
|
||||
Mode: "edit",
|
||||
Server: m.SettingsServers[m.SettingsSelected],
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "d":
|
||||
if m.SettingsConfirmDelete {
|
||||
return m, nil
|
||||
}
|
||||
if len(m.SettingsServers) > 0 {
|
||||
m.SettingsConfirmDelete = true
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case "ctrl+c":
|
||||
m.Quitting = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
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