| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346 |
- 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
- }
|