188 lines
3.6 KiB
Go
188 lines
3.6 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/yeho/doks/internal/search"
|
|
)
|
|
|
|
type Model struct {
|
|
results []search.Result
|
|
cursor int
|
|
selected *search.Result
|
|
query string
|
|
quitting bool
|
|
width int
|
|
height int
|
|
}
|
|
|
|
func New(results []search.Result, query string) Model {
|
|
return Model{
|
|
results: results,
|
|
cursor: 0,
|
|
query: query,
|
|
}
|
|
}
|
|
|
|
func (m Model) Init() tea.Cmd {
|
|
return nil
|
|
}
|
|
|
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
switch msg.String() {
|
|
case "ctrl+c", "q", "esc":
|
|
m.quitting = true
|
|
return m, tea.Quit
|
|
|
|
case "enter":
|
|
if len(m.results) > 0 && m.cursor < len(m.results) {
|
|
m.selected = &m.results[m.cursor]
|
|
}
|
|
return m, tea.Quit
|
|
|
|
case "up", "k":
|
|
if m.cursor > 0 {
|
|
m.cursor--
|
|
}
|
|
|
|
case "down", "j":
|
|
if m.cursor < len(m.results)-1 {
|
|
m.cursor++
|
|
}
|
|
|
|
case "home", "g":
|
|
m.cursor = 0
|
|
|
|
case "end", "G":
|
|
m.cursor = len(m.results) - 1
|
|
}
|
|
|
|
case tea.WindowSizeMsg:
|
|
m.width = msg.Width
|
|
m.height = msg.Height
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (m Model) View() string {
|
|
if len(m.results) == 0 {
|
|
return NoResultsStyle.Render(fmt.Sprintf("No results found for '%s'", m.query))
|
|
}
|
|
|
|
var b strings.Builder
|
|
|
|
// Title
|
|
title := fmt.Sprintf("Search results for '%s' (%d found)", m.query, len(m.results))
|
|
b.WriteString(TitleStyle.Render(title))
|
|
b.WriteString("\n\n")
|
|
|
|
// Results
|
|
visibleResults := m.results
|
|
maxVisible := 10
|
|
if len(visibleResults) > maxVisible {
|
|
// Show results around cursor
|
|
start := m.cursor - maxVisible/2
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
end := start + maxVisible
|
|
if end > len(visibleResults) {
|
|
end = len(visibleResults)
|
|
start = end - maxVisible
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
}
|
|
visibleResults = m.results[start:end]
|
|
}
|
|
|
|
for i, result := range m.results {
|
|
// Check if this result is visible
|
|
visible := false
|
|
for _, vr := range visibleResults {
|
|
if vr.Key == result.Key && vr.LineNumber == result.LineNumber {
|
|
visible = true
|
|
break
|
|
}
|
|
}
|
|
if !visible {
|
|
continue
|
|
}
|
|
|
|
isSelected := i == m.cursor
|
|
|
|
// Cursor indicator
|
|
cursor := " "
|
|
if isSelected {
|
|
cursor = CursorStyle.Render("> ")
|
|
}
|
|
|
|
// Key - bold and bright (even brighter when selected)
|
|
var key string
|
|
if isSelected {
|
|
key = KeyStyleSelected.Render(result.Key)
|
|
} else {
|
|
key = KeyStyle.Render(result.Key)
|
|
}
|
|
|
|
// Line number - dimmer
|
|
var lineNum string
|
|
if isSelected {
|
|
lineNum = LineNumStyleSelected.Render(fmt.Sprintf(":%d", result.LineNumber))
|
|
} else {
|
|
lineNum = LineNumStyle.Render(fmt.Sprintf(":%d", result.LineNumber))
|
|
}
|
|
|
|
// Truncate line text if too long
|
|
lineText := result.LineText
|
|
maxLineLen := 50
|
|
if len(lineText) > maxLineLen {
|
|
lineText = lineText[:maxLineLen-3] + "..."
|
|
}
|
|
|
|
// Match text - dimmer than key
|
|
var matchText string
|
|
if isSelected {
|
|
matchText = MatchTextStyleSelected.Render(lineText)
|
|
} else {
|
|
matchText = MatchTextStyle.Render(lineText)
|
|
}
|
|
|
|
line := fmt.Sprintf("%s%s%s %s", cursor, key, lineNum, matchText)
|
|
b.WriteString(line)
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
// Help
|
|
help := "↑/k up • ↓/j down • enter select • q/esc quit"
|
|
b.WriteString(HelpStyle.Render(help))
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (m Model) Selected() *search.Result {
|
|
return m.selected
|
|
}
|
|
|
|
func (m Model) Quitting() bool {
|
|
return m.quitting
|
|
}
|
|
|
|
func Run(results []search.Result, query string) (*search.Result, error) {
|
|
model := New(results, query)
|
|
p := tea.NewProgram(model)
|
|
|
|
finalModel, err := p.Run()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m := finalModel.(Model)
|
|
return m.Selected(), nil
|
|
}
|