init commit
This commit is contained in:
51
internal/tui/styles.go
Normal file
51
internal/tui/styles.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package tui
|
||||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
var (
|
||||
// Colors
|
||||
secondaryColor = lipgloss.Color("241") // Gray
|
||||
dimColor = lipgloss.Color("245") // Dimmer gray for matched text
|
||||
|
||||
// Styles
|
||||
TitleStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("213")). // Magenta/pink for title
|
||||
MarginBottom(1)
|
||||
|
||||
// Key styles - bold and bright to stand out
|
||||
KeyStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("82")) // Bright green
|
||||
|
||||
KeyStyleSelected = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("154")) // Even brighter green for selected
|
||||
|
||||
LineNumStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("244")) // Medium gray
|
||||
|
||||
LineNumStyleSelected = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("250")) // Lighter for selected
|
||||
|
||||
// Match text - dimmer to not compete with key
|
||||
MatchTextStyle = lipgloss.NewStyle().
|
||||
Foreground(dimColor)
|
||||
|
||||
MatchTextStyleSelected = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("252")) // Slightly brighter when selected
|
||||
|
||||
// Cursor
|
||||
CursorStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("212"))
|
||||
|
||||
HelpStyle = lipgloss.NewStyle().
|
||||
Foreground(secondaryColor).
|
||||
MarginTop(1)
|
||||
|
||||
NoResultsStyle = lipgloss.NewStyle().
|
||||
Foreground(secondaryColor).
|
||||
Italic(true).
|
||||
MarginTop(1)
|
||||
)
|
||||
187
internal/tui/tui.go
Normal file
187
internal/tui/tui.go
Normal file
@@ -0,0 +1,187 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user