package tui import ( "fmt" "strings" tea "github.com/charmbracelet/bubbletea" "github.com/yeho/doks/internal/registry" ) type ListModel struct { entries []*registry.Entry cursor int selected *registry.Entry quitting bool width int height int } func NewListModel(entries []*registry.Entry) ListModel { return ListModel{ entries: entries, cursor: 0, } } func (m ListModel) Init() tea.Cmd { return nil } func (m ListModel) 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.entries) > 0 && m.cursor < len(m.entries) { m.selected = m.entries[m.cursor] } return m, tea.Quit case "up", "k": if m.cursor > 0 { m.cursor-- } case "down", "j": if m.cursor < len(m.entries)-1 { m.cursor++ } case "home", "g": m.cursor = 0 case "end", "G": m.cursor = len(m.entries) - 1 } case tea.WindowSizeMsg: m.width = msg.Width m.height = msg.Height } return m, nil } func (m ListModel) View() string { if len(m.entries) == 0 { return NoResultsStyle.Render("No documents registered") } var b strings.Builder // Title title := fmt.Sprintf("📚 Registered Documents (%d total)", len(m.entries)) b.WriteString(TitleStyle.Render(title)) b.WriteString("\n\n") // Entries with pagination visibleEntries := m.entries maxVisible := 10 startIdx := 0 if len(visibleEntries) > maxVisible { // Show entries around cursor startIdx = m.cursor - maxVisible/2 if startIdx < 0 { startIdx = 0 } endIdx := startIdx + maxVisible if endIdx > len(visibleEntries) { endIdx = len(visibleEntries) startIdx = endIdx - maxVisible if startIdx < 0 { startIdx = 0 } } visibleEntries = m.entries[startIdx:endIdx] } for i, entry := range visibleEntries { actualIdx := startIdx + i isSelected := actualIdx == m.cursor // Cursor indicator cursor := " " if isSelected { cursor = CursorStyle.Render("> ") } // Key - use different styles for selected var key string if isSelected { key = EntryKeySelected.Render(entry.Key) } else { key = EntryKeyStyle.Render(entry.Key) } // Tags var tagsStr string if len(entry.Tags) > 0 { tags := make([]string, len(entry.Tags)) for i, tag := range entry.Tags { tags[i] = "#" + tag } tagsStr = " " + TagsStyle.Render(strings.Join(tags, " ")) } // Symlink indicator var symlinkStr string if entry.HasSymlink { symlinkStr = " 🔗" } // Description (truncated) var descStr string if entry.Description != "" { desc := entry.Description maxDescLen := 40 if len(desc) > maxDescLen { desc = desc[:maxDescLen-3] + "..." } if isSelected { descStr = " " + MatchTextStyleSelected.Render("- "+desc) } else { descStr = " " + MatchTextStyle.Render("- "+desc) } } line := fmt.Sprintf("%s%s%s%s%s", cursor, key, tagsStr, symlinkStr, descStr) b.WriteString(line) b.WriteString("\n") } // Scroll indicator if len(m.entries) > maxVisible { scrollInfo := fmt.Sprintf("\n Showing %d-%d of %d", startIdx+1, min(startIdx+maxVisible, len(m.entries)), len(m.entries)) b.WriteString(CountStyle.Render(scrollInfo)) b.WriteString("\n") } // Help help := "↑/k up • ↓/j down • g/G first/last • enter select • q/esc quit" b.WriteString(HelpStyle.Render(help)) return b.String() } func (m ListModel) Selected() *registry.Entry { return m.selected } func (m ListModel) Quitting() bool { return m.quitting } func RunList(entries []*registry.Entry) (*registry.Entry, error) { model := NewListModel(entries) p := tea.NewProgram(model) finalModel, err := p.Run() if err != nil { return nil, err } m := finalModel.(ListModel) return m.Selected(), nil }