package main import ( "flag" "fmt" "os" "time" "envguard/internal/api" "envguard/internal/models" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var baseStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")) type model struct { table table.Model services []models.Service user string err error message string width int height int } func (m model) Init() tea.Cmd { return fetchServices } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "esc", "q", "ctrl+c": return m, tea.Quit case "enter": selected := m.table.SelectedRow() if selected == nil { return m, nil } serviceName := selected[0] // Find service state for _, s := range m.services { if s.Name == serviceName { if s.IsLocked { if s.LockedBy == m.user { // Unlock err := api.UnlockService(serviceName, m.user) if err != nil { m.message = "Error unlocking: " + err.Error() } else { m.message = "Unlocked " + serviceName } } else { m.message = "Cannot unlock service locked by " + s.LockedBy } } else { // Lock err := api.LockService(serviceName, m.user) if err != nil { m.message = "Error locking: " + err.Error() } else { m.message = "Locked " + serviceName } } return m, fetchServices } } case "r": return m, fetchServices } case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height // Calculate available dimensions // Borders take 2 chars total width (1 left + 1 right) // We add extra safety margin (total -6) to avoid last-column wrap issues availWidth := m.width - 10 // Adjust column widths dynamically // Total weight = 100% cols := m.table.Columns() // Calculate widths ensuring at least minimums c0 := int(float64(availWidth) * 0.3) c1 := int(float64(availWidth) * 0.2) c2 := int(float64(availWidth) * 0.2) c3 := availWidth - c0 - c1 - c2 cols[0].Width = c0 cols[1].Width = c1 cols[2].Width = c2 cols[3].Width = c3 m.table.SetColumns(cols) m.table.SetWidth(availWidth) // Header + Footer + padding calculation availableHeight := m.height - 7 if availableHeight < 5 { availableHeight = 5 } m.table.SetHeight(availableHeight) case []models.Service: m.services = msg rows := []table.Row{} for _, s := range msg { status := "🟢 LIBRE" if s.IsLocked { status = "🔴 LOCKED" } user := s.LockedBy if user == "" { user = "--" } timeStr := "--" if s.IsLocked { duration := time.Since(s.LockedAt) timeStr = fmt.Sprintf("Hace %s", duration.Round(time.Second)) } rows = append(rows, table.Row{s.Name, status, user, timeStr}) } m.table.SetRows(rows) m.message = "Refreshed" case error: m.err = msg } m.table, cmd = m.table.Update(msg) return m, cmd } func (m model) View() string { if m.err != nil { return fmt.Sprintf("Error: %v\nPress q to quit", m.err) } if m.width == 0 { return "Loading..." } userInfo := fmt.Sprintf("User: %s", m.user) if m.message != "" { userInfo += " | " + m.message } // Dimensions appWidth := m.width - 2 appHeight := m.height - 2 if appWidth < 20 { appWidth = 20 } if appHeight < 10 { appHeight = 10 } // Content inside border contentWidth := appWidth - 2 // Local Styles headerStyle := lipgloss.NewStyle(). Bold(true). Foreground(lipgloss.Color("252")). PaddingLeft(1). PaddingBottom(1). Width(contentWidth) infoStyle := lipgloss.NewStyle(). PaddingLeft(1). Foreground(lipgloss.Color("241")). Width(contentWidth) footerStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("240")). Align(lipgloss.Center). Width(contentWidth) // Render Content content := lipgloss.JoinVertical(lipgloss.Left, headerStyle.Render("🛡️ ENV-GUARD v1.0"), infoStyle.Render(userInfo), m.table.View(), footerStyle.Render("[↑/↓] Navigate [Enter] Toggle Lock [r] Refresh [q] Quit"), ) // Create Border Style locally borderStyle := lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")). Width(appWidth). Height(appHeight - 2) // Wrap in Border gui := borderStyle.Render(content) // Place in center of screen return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, gui) } func fetchServices() tea.Msg { services, err := api.GetServices() if err != nil { return err } return services } func main() { serverURL := flag.String("server", "http://localhost:8080", "URL of the EnvGuard server") flag.Parse() api.SetBaseURL(*serverURL) user := os.Getenv("USER") if user == "" { user = "guest" } columns := []table.Column{ {Title: "SERVICE", Width: 20}, {Title: "STATUS", Width: 15}, {Title: "USER", Width: 15}, {Title: "TIME", Width: 20}, } t := table.New( table.WithColumns(columns), table.WithFocused(true), table.WithHeight(10), ) s := table.DefaultStyles() s.Header = s.Header. BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("240")). BorderBottom(true). Bold(true) s.Selected = s.Selected. Foreground(lipgloss.Color("229")). Background(lipgloss.Color("57")). Bold(false) t.SetStyles(s) m := model{ table: t, user: user, } if _, err := tea.NewProgram(m, tea.WithAltScreen()).Run(); err != nil { fmt.Println("Error running program:", err) os.Exit(1) } }