312 lines
6.6 KiB
Go
312 lines
6.6 KiB
Go
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
|
|
}
|