arguments.go

  1package commands
  2
  3import (
  4	"cmp"
  5
  6	"github.com/charmbracelet/bubbles/v2/help"
  7	"github.com/charmbracelet/bubbles/v2/key"
  8	"github.com/charmbracelet/bubbles/v2/textinput"
  9	tea "github.com/charmbracelet/bubbletea/v2"
 10	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 11	"github.com/charmbracelet/crush/internal/tui/styles"
 12	"github.com/charmbracelet/crush/internal/tui/util"
 13	"github.com/charmbracelet/lipgloss/v2"
 14)
 15
 16const (
 17	argumentsDialogID dialogs.DialogID = "arguments"
 18)
 19
 20// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
 21type ShowArgumentsDialogMsg struct {
 22	CommandID   string
 23	Description string
 24	Content     string
 25	ArgNames    []string
 26}
 27
 28// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
 29type CloseArgumentsDialogMsg struct {
 30	Submit    bool
 31	CommandID string
 32	Content   string
 33	Args      map[string]string
 34}
 35
 36// CommandArgumentsDialog represents the commands dialog.
 37type CommandArgumentsDialog interface {
 38	dialogs.DialogModel
 39}
 40
 41type commandArgumentsDialogCmp struct {
 42	wWidth, wHeight int
 43	width, height   int
 44
 45	inputs    []textinput.Model
 46	focused   int
 47	keys      ArgumentsDialogKeyMap
 48	arguments []Argument
 49	help      help.Model
 50
 51	id          string
 52	title       string
 53	name        string
 54	description string
 55
 56	onSubmit func(args map[string]string) tea.Cmd
 57}
 58
 59type Argument struct {
 60	Name, Title, Description string
 61	Required                 bool
 62}
 63
 64func NewCommandArgumentsDialog(
 65	id, title, name, description string,
 66	arguments []Argument,
 67	onSubmit func(args map[string]string) tea.Cmd,
 68) CommandArgumentsDialog {
 69	t := styles.CurrentTheme()
 70	inputs := make([]textinput.Model, len(arguments))
 71
 72	for i, arg := range arguments {
 73		ti := textinput.New()
 74		ti.Placeholder = cmp.Or(arg.Description, "Enter value for "+arg.Title)
 75		ti.SetWidth(40)
 76		ti.SetVirtualCursor(false)
 77		ti.Prompt = ""
 78
 79		ti.SetStyles(t.S().TextInput)
 80		// Only focus the first input initially
 81		if i == 0 {
 82			ti.Focus()
 83		} else {
 84			ti.Blur()
 85		}
 86
 87		inputs[i] = ti
 88	}
 89
 90	return &commandArgumentsDialogCmp{
 91		inputs:      inputs,
 92		keys:        DefaultArgumentsDialogKeyMap(),
 93		id:          id,
 94		name:        name,
 95		title:       title,
 96		description: description,
 97		arguments:   arguments,
 98		width:       60,
 99		help:        help.New(),
100		onSubmit:    onSubmit,
101	}
102}
103
104// Init implements CommandArgumentsDialog.
105func (c *commandArgumentsDialogCmp) Init() tea.Cmd {
106	return nil
107}
108
109// Update implements CommandArgumentsDialog.
110func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
111	switch msg := msg.(type) {
112	case tea.WindowSizeMsg:
113		c.wWidth = msg.Width
114		c.wHeight = msg.Height
115		c.width = min(90, c.wWidth)
116		c.height = min(15, c.wHeight)
117		for i := range c.inputs {
118			c.inputs[i].SetWidth(c.width - (paddingHorizontal * 2))
119		}
120	case tea.KeyPressMsg:
121		switch {
122		case key.Matches(msg, c.keys.Cancel):
123			return c, util.CmdHandler(dialogs.CloseDialogMsg{})
124		case key.Matches(msg, c.keys.Confirm):
125			if c.focused == len(c.inputs)-1 {
126				args := make(map[string]string)
127				for i, arg := range c.arguments {
128					value := c.inputs[i].Value()
129					args[arg.Name] = value
130				}
131				return c, tea.Sequence(
132					util.CmdHandler(dialogs.CloseDialogMsg{}),
133					c.onSubmit(args),
134				)
135			}
136			// Otherwise, move to the next input
137			c.inputs[c.focused].Blur()
138			c.focused++
139			c.inputs[c.focused].Focus()
140		case key.Matches(msg, c.keys.Next):
141			// Move to the next input
142			c.inputs[c.focused].Blur()
143			c.focused = (c.focused + 1) % len(c.inputs)
144			c.inputs[c.focused].Focus()
145		case key.Matches(msg, c.keys.Previous):
146			// Move to the previous input
147			c.inputs[c.focused].Blur()
148			c.focused = (c.focused - 1 + len(c.inputs)) % len(c.inputs)
149			c.inputs[c.focused].Focus()
150
151		default:
152			var cmd tea.Cmd
153			c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
154			return c, cmd
155		}
156	}
157	return c, nil
158}
159
160// View implements CommandArgumentsDialog.
161func (c *commandArgumentsDialogCmp) View() string {
162	t := styles.CurrentTheme()
163	baseStyle := t.S().Base
164
165	title := lipgloss.NewStyle().
166		Foreground(t.Primary).
167		Bold(true).
168		Padding(0, 1).
169		Render(cmp.Or(c.title, c.name))
170
171	promptName := t.S().Text.
172		Padding(0, 1).
173		Render(c.description)
174
175	inputFields := make([]string, len(c.inputs))
176	for i, input := range c.inputs {
177		labelStyle := baseStyle.Padding(1, 1, 0, 1)
178
179		if i == c.focused {
180			labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
181		} else {
182			labelStyle = labelStyle.Foreground(t.FgMuted)
183		}
184
185		arg := c.arguments[i]
186		argName := cmp.Or(arg.Title, arg.Name)
187		if arg.Required {
188			argName += "*"
189		}
190		label := labelStyle.Render(argName + ":")
191
192		field := t.S().Text.
193			Padding(0, 1).
194			Render(input.View())
195
196		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
197	}
198
199	elements := []string{title, promptName}
200	elements = append(elements, inputFields...)
201
202	c.help.ShowAll = false
203	helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
204	elements = append(elements, "", helpText)
205
206	content := lipgloss.JoinVertical(lipgloss.Left, elements...)
207
208	return baseStyle.Padding(1, 1, 0, 1).
209		Border(lipgloss.RoundedBorder()).
210		BorderForeground(t.BorderFocus).
211		Width(c.width).
212		Render(content)
213}
214
215func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
216	if len(c.inputs) == 0 {
217		return nil
218	}
219	cursor := c.inputs[c.focused].Cursor()
220	if cursor != nil {
221		cursor = c.moveCursor(cursor)
222	}
223	return cursor
224}
225
226const (
227	headerHeight      = 3
228	itemHeight        = 3
229	paddingHorizontal = 3
230)
231
232func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
233	row, col := c.Position()
234	offset := row + headerHeight + (1+c.focused)*itemHeight
235	cursor.Y += offset
236	cursor.X = cursor.X + col + paddingHorizontal
237	return cursor
238}
239
240func (c *commandArgumentsDialogCmp) Position() (int, int) {
241	row := (c.wHeight / 2) - (c.height / 2)
242	col := (c.wWidth / 2) - (c.width / 2)
243	return row, col
244}
245
246// ID implements CommandArgumentsDialog.
247func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID {
248	return argumentsDialogID
249}