1package dialog
2
3import (
4 "strings"
5
6 "charm.land/lipgloss/v2"
7 "github.com/charmbracelet/crush/internal/ui/list"
8 "github.com/charmbracelet/crush/internal/ui/styles"
9 "github.com/charmbracelet/x/ansi"
10 "github.com/sahilm/fuzzy"
11)
12
13// CommandItem wraps a uicmd.Command to implement the ListItem interface.
14type CommandItem struct {
15 *list.Versioned
16 id string
17 title string
18 shortcut string
19 description string
20 action Action
21 aliases []string
22 t *styles.Styles
23 m fuzzy.Match
24 cache map[int]string
25 focused bool
26}
27
28var _ ListItem = &CommandItem{Versioned: list.NewVersioned()}
29
30// NewCommandItem creates a new CommandItem.
31func NewCommandItem(t *styles.Styles, id, title, shortcut string, action Action) *CommandItem {
32 return &CommandItem{
33 Versioned: list.NewVersioned(),
34 id: id,
35 t: t,
36 title: title,
37 shortcut: shortcut,
38 action: action,
39 }
40}
41
42// Finished implements list.Item. Command items are render-stable
43// outside of explicit SetFocused / SetMatch.
44func (c *CommandItem) Finished() bool {
45 return true
46}
47
48// WithAliases returns the CommandItem with the given aliases for filtering.
49func (c *CommandItem) WithAliases(aliases ...string) *CommandItem {
50 c.aliases = aliases
51 return c
52}
53
54// WithDescription returns the CommandItem with a description displayed below
55// the title.
56func (c *CommandItem) WithDescription(desc string) *CommandItem {
57 c.description = desc
58 return c
59}
60
61// Filter implements ListItem.
62func (c *CommandItem) Filter() string {
63 base := c.title
64 if len(c.aliases) > 0 {
65 base = c.title + " " + strings.Join(c.aliases, " ")
66 }
67 if c.description != "" {
68 base = base + " " + c.description
69 }
70 return base
71}
72
73// ID implements ListItem.
74func (c *CommandItem) ID() string {
75 return c.id
76}
77
78// SetFocused implements ListItem.
79func (c *CommandItem) SetFocused(focused bool) {
80 if c.focused == focused {
81 return
82 }
83 c.cache = nil
84 c.focused = focused
85 if c.Versioned != nil {
86 c.Bump()
87 }
88}
89
90// SetMatch implements ListItem.
91func (c *CommandItem) SetMatch(m fuzzy.Match) {
92 if sameFuzzyMatch(c.m, m) {
93 return
94 }
95 c.cache = nil
96 c.m = m
97 if c.Versioned != nil {
98 c.Bump()
99 }
100}
101
102// Action returns the action associated with the command item.
103func (c *CommandItem) Action() Action {
104 return c.action
105}
106
107// Shortcut returns the shortcut associated with the command item.
108func (c *CommandItem) Shortcut() string {
109 return c.shortcut
110}
111
112// Render implements ListItem.
113func (c *CommandItem) Render(width int) string {
114 styles := ListItemStyles{
115 ItemBlurred: c.t.Dialog.NormalItem,
116 ItemFocused: c.t.Dialog.SelectedItem,
117 InfoTextBlurred: c.t.Dialog.ListItem.InfoBlurred,
118 InfoTextFocused: c.t.Dialog.ListItem.InfoFocused,
119 }
120 rendered := renderItem(styles, c.title, c.shortcut, c.focused, width, c.cache, &c.m)
121 if c.description != "" {
122 descStyle := c.t.Dialog.SecondaryText
123 if c.focused {
124 descStyle = c.t.Dialog.SelectedItem
125 }
126 contentWidth := max(0, width-descStyle.GetHorizontalFrameSize()+1)
127 description := ansi.Truncate(strings.TrimSpace(c.description), contentWidth, "...")
128 descVisWidth := lipgloss.Width(description)
129 gap := strings.Repeat(" ", max(0, contentWidth-descVisWidth))
130 if description == "" {
131 description = " "
132 }
133 rendered = lipgloss.JoinVertical(lipgloss.Left, rendered, descStyle.Render(description+gap))
134 }
135 return rendered
136}