skills.go

  1package model
  2
  3import (
  4	"fmt"
  5	"path/filepath"
  6	"slices"
  7	"strings"
  8	"sync"
  9
 10	"charm.land/lipgloss/v2"
 11	"github.com/charmbracelet/crush/internal/skills"
 12	"github.com/charmbracelet/crush/internal/ui/common"
 13	"github.com/charmbracelet/crush/internal/ui/styles"
 14)
 15
 16type skillStatusItem struct {
 17	icon  string
 18	name  string
 19	title string
 20	// description is reserved for future use (e.g. showing error details).
 21	description string
 22}
 23
 24var builtinSkillsCache struct {
 25	once   sync.Once
 26	skills []*skills.Skill
 27}
 28
 29func cachedBuiltinSkills() []*skills.Skill {
 30	builtinSkillsCache.once.Do(func() {
 31		builtinSkillsCache.skills = skills.DiscoverBuiltin()
 32	})
 33	return builtinSkillsCache.skills
 34}
 35
 36// skillsInfo renders the skill discovery status section showing loaded and
 37// invalid skills.
 38func (m *UI) skillsInfo(width, maxItems int, isSection bool) string {
 39	t := m.com.Styles
 40
 41	title := t.ResourceGroupTitle.Render("Skills")
 42	if isSection {
 43		title = common.Section(t, title, width)
 44	}
 45
 46	items := m.skillStatusItems()
 47	if len(items) == 0 {
 48		list := t.ResourceAdditionalText.Render("None")
 49		return lipgloss.NewStyle().Width(width).Render(fmt.Sprintf("%s\n\n%s", title, list))
 50	}
 51
 52	list := skillsList(t, items, width, maxItems)
 53	return lipgloss.NewStyle().Width(width).Render(fmt.Sprintf("%s\n\n%s", title, list))
 54}
 55
 56func (m *UI) skillStatusItems() []skillStatusItem {
 57	t := m.com.Styles
 58	var items []skillStatusItem
 59	stateNames := make(map[string]struct{}, len(m.skillStates))
 60
 61	states := slices.Clone(m.skillStates)
 62	slices.SortStableFunc(states, func(a, b *skills.SkillState) int {
 63		return strings.Compare(a.Path, b.Path)
 64	})
 65	for _, state := range states {
 66		name := state.Name
 67		if name == "" {
 68			name = filepath.Base(filepath.Dir(state.Path))
 69		}
 70		stateNames[name] = struct{}{}
 71		icon := t.ResourceOnlineIcon.String()
 72		if state.State == skills.StateError {
 73			icon = t.ResourceErrorIcon.String()
 74		}
 75		items = append(items, skillStatusItem{
 76			icon:  icon,
 77			name:  name,
 78			title: t.ResourceName.Render(name),
 79		})
 80	}
 81
 82	builtin := cachedBuiltinSkills()
 83	slices.SortStableFunc(builtin, func(a, b *skills.Skill) int {
 84		return strings.Compare(a.Name, b.Name)
 85	})
 86	for _, skill := range builtin {
 87		if _, ok := stateNames[skill.Name]; ok {
 88			continue
 89		}
 90		items = append(items, skillStatusItem{
 91			icon:  t.ResourceOnlineIcon.String(),
 92			name:  skill.Name,
 93			title: t.ResourceName.Render(skill.Name),
 94		})
 95	}
 96
 97	slices.SortStableFunc(items, func(a, b skillStatusItem) int {
 98		return strings.Compare(a.name, b.name)
 99	})
100
101	return items
102}
103
104func skillsList(t *styles.Styles, items []skillStatusItem, width, maxItems int) string {
105	if maxItems <= 0 {
106		return ""
107	}
108
109	if len(items) > maxItems {
110		visibleItems := items[:maxItems-1]
111		remaining := len(items) - (maxItems - 1)
112		items = append(visibleItems, skillStatusItem{
113			name:  "more",
114			title: t.ResourceAdditionalText.Render(fmt.Sprintf("…and %d more", remaining)),
115		})
116	}
117
118	renderedItems := make([]string, 0, len(items))
119	for _, item := range items {
120		renderedItems = append(renderedItems, common.Status(t, common.StatusOpts{
121			Icon:        item.icon,
122			Title:       item.title,
123			Description: item.description,
124		}, width))
125	}
126	return lipgloss.JoinVertical(lipgloss.Left, renderedItems...)
127}