Add WireGuard TUI implementation
- Add Go TUI with bubbletea for WireGuard management - Implement client CRUD operations with QR code generation - Add configuration and validation modules - Install/update scripts for client setup - Update Makefile to build binaries to bin/ directory - Add .gitignore for Go projects
This commit is contained in:
307
internal/tui/components/search.go
Normal file
307
internal/tui/components/search.go
Normal file
@@ -0,0 +1,307 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"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
|
||||
var (
|
||||
searchBarStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("255")).
|
||||
Background(lipgloss.Color("235")).
|
||||
Padding(0, 1).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("240"))
|
||||
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+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
|
||||
}
|
||||
Reference in New Issue
Block a user