package components import ( "github.com/calmcacil/wg-admin/internal/tui/theme" "strconv" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // SearchFilterType represents the type of filter type SearchFilterType string const ( FilterByName SearchFilterType = "name" FilterByIPv4 SearchFilterType = "ipv4" FilterByIPv6 SearchFilterType = "ipv6" FilterByStatus SearchFilterType = "status" ) // SearchModel represents the search component type SearchModel struct { input textinput.Model active bool filterType SearchFilterType matchCount int totalCount int visible bool } // Styles // Styles (using theme package) var ( searchBarStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("255")). Background(theme.StyleBackground). Padding(0, 1). Border(lipgloss.RoundedBorder()). BorderForeground(theme.StyleBorder) searchPromptStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("226")). Bold(true) searchFilterStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("147")) searchCountStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("241")) searchHelpStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("243")) ) // NewSearch creates a new search component func NewSearch() *SearchModel { ti := textinput.New() ti.Placeholder = "Search clients..." ti.Focus() ti.CharLimit = 156 ti.Width = 40 ti.Prompt = "" return &SearchModel{ input: ti, active: false, filterType: FilterByName, matchCount: 0, totalCount: 0, visible: true, } } // Init initializes the search component func (m *SearchModel) Init() tea.Cmd { return nil } // Update handles messages for the search component func (m *SearchModel) Update(msg tea.Msg) (*SearchModel, tea.Cmd) { if !m.active { return m, nil } var cmd tea.Cmd switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "esc": m.active = false m.input.Reset() m.matchCount = m.totalCount return m, nil case "tab": m.cycleFilterType() return m, nil } } m.input, cmd = m.input.Update(msg) return m, cmd } // View renders the search component func (m *SearchModel) View() string { if !m.visible { return "" } var filterLabel string switch m.filterType { case FilterByName: filterLabel = "Name" case FilterByIPv4: filterLabel = "IPv4" case FilterByIPv6: filterLabel = "IPv6" case FilterByStatus: filterLabel = "Status" } searchIndicator := "" if m.active { searchIndicator = searchPromptStyle.Render("🔍 ") } else { searchIndicator = searchPromptStyle.Render("⌕ ") } filterText := searchFilterStyle.Render("[" + filterLabel + "]") countText := "" if m.totalCount > 0 { countText = searchCountStyle.Render( lipgloss.JoinHorizontal( lipgloss.Left, strings.Repeat(" ", 4), "Matched: ", m.renderCount(m.matchCount), "/", m.renderCount(m.totalCount), ), ) } helpText := "" if m.active { helpText = searchHelpStyle.Render(" | Tab: filter | Esc: clear") } else { helpText = searchHelpStyle.Render(" | /: search") } content := lipgloss.JoinHorizontal( lipgloss.Left, searchIndicator, m.input.View(), filterText, countText, helpText, ) return searchBarStyle.Render(content) } // IsActive returns true if search is active func (m *SearchModel) IsActive() bool { return m.active } // Activate activates the search func (m *SearchModel) Activate() { m.active = true m.input.Focus() } // Deactivate deactivates the search func (m *SearchModel) Deactivate() { m.active = false m.input.Reset() m.matchCount = m.totalCount } // Clear clears the search input and filter func (m *SearchModel) Clear() { m.input.Reset() m.matchCount = m.totalCount } // GetQuery returns the current search query func (m *SearchModel) GetQuery() string { return m.input.Value() } // GetFilterType returns the current filter type func (m *SearchModel) GetFilterType() SearchFilterType { return m.filterType } // SetTotalCount sets the total number of items func (m *SearchModel) SetTotalCount(count int) { m.totalCount = count if !m.active { m.matchCount = count } } // SetMatchCount sets the number of matching items func (m *SearchModel) SetMatchCount(count int) { m.matchCount = count } // Filter filters a list of client data based on the current search query func (m *SearchModel) Filter(clients []ClientData) []ClientData { query := strings.TrimSpace(m.input.Value()) if query == "" || !m.active { m.matchCount = len(clients) return clients } var filtered []ClientData queryLower := strings.ToLower(query) for _, client := range clients { var matches bool switch m.filterType { case FilterByName: matches = strings.Contains(strings.ToLower(client.Name), queryLower) case FilterByIPv4: matches = strings.Contains(strings.ToLower(client.IPv4), queryLower) case FilterByIPv6: matches = strings.Contains(strings.ToLower(client.IPv6), queryLower) case FilterByStatus: matches = strings.Contains(strings.ToLower(client.Status), queryLower) } if matches { filtered = append(filtered, client) } } m.matchCount = len(filtered) return filtered } // HighlightMatches highlights matching text in the given value func (m *SearchModel) HighlightMatches(value string) string { if !m.active { return value } query := strings.TrimSpace(m.input.Value()) if query == "" { return value } queryLower := strings.ToLower(query) valueLower := strings.ToLower(value) index := strings.Index(valueLower, queryLower) if index == -1 { return value } matchStyle := lipgloss.NewStyle(). Foreground(lipgloss.Color("226")). Background(lipgloss.Color("57")). Bold(true) before := value[:index] match := value[index : index+len(query)] after := value[index+len(query):] return lipgloss.JoinHorizontal( lipgloss.Left, before, matchStyle.Render(string(match)), after, ) } // cycleFilterType cycles to the next filter type func (m *SearchModel) cycleFilterType() { switch m.filterType { case FilterByName: m.filterType = FilterByIPv4 case FilterByIPv4: m.filterType = FilterByIPv6 case FilterByIPv6: m.filterType = FilterByStatus case FilterByStatus: m.filterType = FilterByName } } // renderCount renders a count number with proper styling func (m *SearchModel) renderCount(count int) string { if m.matchCount == 0 && m.active && m.input.Value() != "" { return lipgloss.NewStyle(). Foreground(lipgloss.Color("196")). Render("No matches") } return searchCountStyle.Render(string(rune('0' + count))) } // ClientData represents client data for filtering type ClientData struct { Name string IPv4 string IPv6 string Status string }