commands_item.go

  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}