feat: interactive list when running empty root command

This commit is contained in:
Yehoshua Adam Sandler
2026-02-04 12:16:28 -06:00
parent d24e4f340e
commit 42ced28ea7
3 changed files with 226 additions and 2 deletions

View File

@@ -1 +1 @@
0.1.1
0.1.2

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"os/exec"
"sort"
"github.com/spf13/cobra"
"github.com/yeho/doks/internal/config"
@@ -39,7 +40,7 @@ and manuals. Access documents by key or search through content.`,
searchDocuments(searchFlag)
return
}
cmd.Help()
showInteractiveList()
},
}
@@ -203,3 +204,33 @@ func displayFile(filePath string, highlightLine int) {
}
fmt.Print(string(content))
}
func showInteractiveList() {
reg, err := registry.Load(cfg.RegistryPath())
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading registry: %v\n", err)
os.Exit(1)
}
entries := reg.List()
if len(entries) == 0 {
fmt.Println(tui.NoResultsStyle.Render("No documents registered. Use 'doks add <file>' to add documents."))
return
}
// Sort by key
sort.Slice(entries, func(i, j int) bool {
return entries[i].Key < entries[j].Key
})
selected, err := tui.RunList(entries)
if err != nil {
fmt.Fprintf(os.Stderr, "Error running TUI: %v\n", err)
os.Exit(1)
}
if selected != nil {
filePath := cfg.FilePath(selected.Filename)
displayFile(filePath, 0)
}
}

193
internal/tui/list.go Normal file
View File

@@ -0,0 +1,193 @@
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
}