feat: interactive list when running empty root command
This commit is contained in:
33
cmd/root.go
33
cmd/root.go
@@ -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
193
internal/tui/list.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user