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}