package main import ( "fmt" "io" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/lipgloss" ) // Constants for UI dimensions const ( minEmailWidth = 50 maxEmailWidth = 70 minLoadingWidth = 40 maxLoadingWidth = 60 inputCharLimit = 256 inputWidth = 50 listHeightOffset = 2 ) // Color scheme var ( primaryColor = lipgloss.Color("205") secondaryColor = lipgloss.Color("240") successColor = lipgloss.Color("42") subtleColor = lipgloss.Color("241") borderColor = lipgloss.Color("236") errorColor = lipgloss.Color("196") ) // UI style definitions grouped by purpose var ( // Text styles titleStyle = lipgloss.NewStyle(). Bold(true). Foreground(primaryColor). MarginBottom(1) labelStyle = lipgloss.NewStyle(). Foreground(secondaryColor). MarginRight(1) helpTextStyle = lipgloss.NewStyle(). Foreground(subtleColor). Italic(true). MarginTop(1) loadingStyle = lipgloss.NewStyle(). Foreground(primaryColor). Margin(1, 0) errorStyle = lipgloss.NewStyle(). Foreground(errorColor). Bold(true) // Container styles containerStyle = lipgloss.NewStyle(). Border(lipgloss.RoundedBorder()). BorderForeground(borderColor). Background(lipgloss.NoColor{}). Padding(1, 2). Margin(1, 0) // Checkbox styles checkboxStyle = lipgloss.NewStyle(). Foreground(primaryColor). MarginRight(1) checkboxCheckedStyle = lipgloss.NewStyle(). Foreground(successColor). MarginRight(1) // Status styles statusConnectedStyle = lipgloss.NewStyle(). Foreground(successColor). Bold(true) statusDisconnectedStyle = lipgloss.NewStyle(). Foreground(subtleColor) ) // listKeyMap defines custom key bindings for the list view. type listKeyMap struct { connect key.Binding disconnect key.Binding disconnAll key.Binding refresh key.Binding } var customKeys = listKeyMap{ connect: key.NewBinding( key.WithKeys("enter"), key.WithHelp("enter", "connect"), ), disconnect: key.NewBinding( key.WithKeys("d"), key.WithHelp("d", "disconnect"), ), disconnAll: key.NewBinding( key.WithKeys("ctrl+x"), key.WithHelp("ctrl+x", "disconnect all"), ), refresh: key.NewBinding( key.WithKeys("r"), key.WithHelp("r", "refresh"), ), } // item represents a connection item in the list view. type item struct { name string address string connected bool connType string } // Title returns the display title for the item, with a bullet indicator if connected. func (i item) Title() string { if i.connected { return "● " + i.name } return i.name } // Description returns the formatted description showing the address and connection status. func (i item) Description() string { addressStyle := statusDisconnectedStyle prefix := "" if i.connType != "" { prefix = "[" + i.connType + "] " } if i.connected { addressStyle = statusConnectedStyle return addressStyle.Render(prefix+i.address) + " " + statusConnectedStyle.Render("• Connected") } return addressStyle.Render(prefix + i.address) } // FilterValue returns the value used for filtering items in the list. func (i item) FilterValue() string { return i.name } // customDelegate provides custom rendering for list items with connection status styling. type customDelegate struct { list.DefaultDelegate } // Render renders a list item with custom styling based on selection and connection status. func (d customDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { itm, ok := listItem.(item) if !ok { d.DefaultDelegate.Render(w, m, index, listItem) return } isSelected := index == m.Index() titleStyle := d.getTitleStyle(isSelected, itm.connected) title := titleStyle.Render(itm.Title()) descStyle := d.Styles.NormalDesc if isSelected { descStyle = d.Styles.SelectedDesc } desc := descStyle.Render(itm.Description()) fmt.Fprint(w, lipgloss.JoinVertical(lipgloss.Left, title, desc)) } // getTitleStyle returns the appropriate title style based on selection and connection status. func (d customDelegate) getTitleStyle(isSelected, isConnected bool) lipgloss.Style { baseStyle := lipgloss.NewStyle(). Background(lipgloss.NoColor{}). Padding(0, 0, 0, 1) if isSelected { selectedStyle := d.Styles.SelectedTitle. Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(primaryColor). Bold(true). Background(lipgloss.NoColor{}). Padding(0, 0, 0, 1) if isConnected { return selectedStyle.Foreground(successColor) } return selectedStyle.Foreground(primaryColor) } // Normal (unselected) item if isConnected { return baseStyle. Foreground(successColor). Bold(true) } return baseStyle.Foreground(lipgloss.Color("252")) } // newList creates and configures a new list model with custom styling. func newList(items []list.Item, width, height int) list.Model { delegate := customDelegate{DefaultDelegate: list.NewDefaultDelegate()} delegate.Styles.SelectedTitle = delegate.Styles.SelectedTitle. Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(primaryColor). Foreground(primaryColor). Bold(true). Background(lipgloss.NoColor{}). Padding(0, 0, 0, 1) delegate.Styles.SelectedDesc = delegate.Styles.SelectedDesc. Border(lipgloss.NormalBorder(), false, false, false, true). BorderForeground(primaryColor). Foreground(secondaryColor). Background(lipgloss.NoColor{}). Padding(0, 0, 0, 1) delegate.Styles.NormalDesc = delegate.Styles.NormalDesc. Foreground(subtleColor). Background(lipgloss.NoColor{}). Padding(0, 0, 0, 1) l := list.New(items, delegate, width, height) l.Title = "" l.SetShowStatusBar(false) l.SetShowPagination(false) l.SetFilteringEnabled(true) l.SetShowHelp(true) l.AdditionalShortHelpKeys = func() []key.Binding { return []key.Binding{ customKeys.connect, customKeys.disconnect, customKeys.disconnAll, customKeys.refresh, } } l.Styles.StatusBar = lipgloss.NewStyle(). Foreground(subtleColor). Background(lipgloss.NoColor{}). MarginTop(1) l.Styles.Title = lipgloss.NewStyle(). Background(lipgloss.NoColor{}) l.Styles.FilterPrompt = lipgloss.NewStyle(). Background(lipgloss.NoColor{}) l.Styles.FilterCursor = lipgloss.NewStyle(). Background(lipgloss.NoColor{}) return l } // isQuitKey checks if the given key string represents a quit command. func isQuitKey(key string) bool { return key == "q" || key == "ctrl+c" || key == "esc" } // renderEmailPrompt renders the email authentication prompt view. func renderEmailPrompt(m model) string { if m.loggingIn { title := titleStyle.Render("Authenticating...") spinnerText := loadingStyle.Render(m.spinner.View() + " Opening browser for authentication...") content := lipgloss.JoinVertical(lipgloss.Center, "", title, "", spinnerText, "", ) width := clampWidth(m.width, minLoadingWidth, maxLoadingWidth) return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, containerStyle.Width(width).Align(lipgloss.Center).Render(content)) } checkbox := checkboxStyle.Render("[ ]") if m.rememberEmail { checkbox = checkboxCheckedStyle.Render("[x]") } title := titleStyle.Render("Authentication Required") message := labelStyle.Render("You are not authenticated. Please login.") emailLabel := labelStyle.Render("Email:") emailInput := m.emailInput.View() rememberText := checkbox + labelStyle.Render("Remember email (press Space to toggle)") helpText := helpTextStyle.Render("Press Enter to login, Esc to quit") content := lipgloss.JoinVertical(lipgloss.Left, title, "", message, "", emailLabel+emailInput, "", rememberText, "", helpText, ) width := clampWidth(m.width, minEmailWidth, maxEmailWidth) return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, containerStyle.Width(width).Render(content)) } // renderLoading renders the loading spinner view. func renderLoading(m model) string { title := titleStyle.Render("Loading Connections") spinnerText := loadingStyle.Render(m.spinner.View() + " Loading connections...") content := lipgloss.JoinVertical(lipgloss.Center, "", title, "", spinnerText, "", ) width := clampWidth(m.width, minLoadingWidth, maxLoadingWidth) return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, containerStyle.Width(width).Align(lipgloss.Center).Render(content)) } // renderError renders the error view with retry and quit options. func renderError(m model) string { title := titleStyle.Render("Error") errText := errorStyle.Render(m.err) helpText := helpTextStyle.Render("Press r to retry, q/Esc to quit") content := lipgloss.JoinVertical(lipgloss.Center, "", title, "", errText, "", helpText, "", ) width := clampWidth(m.width, minLoadingWidth, maxLoadingWidth) return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, containerStyle.Width(width).Align(lipgloss.Center).Render(content)) } // clampWidth constrains the width value between min and max. func clampWidth(width, min, max int) int { if width > max { return max } if width < min { return min } return width }