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	ArgNames    []string
 25	OnSubmit    func(args map[string]string) tea.Cmd
 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.Confirm):
123			if c.focused == len(c.inputs)-1 {
124				args := make(map[string]string)
125				for i, arg := range c.arguments {
126					value := c.inputs[i].Value()
127					args[arg.Name] = value
128				}
129				return c, tea.Sequence(
130					util.CmdHandler(dialogs.CloseDialogMsg{}),
131					c.onSubmit(args),
132				)
133			}
134			// Otherwise, move to the next input
135			c.inputs[c.focused].Blur()
136			c.focused++
137			c.inputs[c.focused].Focus()
138		case key.Matches(msg, c.keys.Next):
139			// Move to the next input
140			c.inputs[c.focused].Blur()
141			c.focused = (c.focused + 1) % len(c.inputs)
142			c.inputs[c.focused].Focus()
143		case key.Matches(msg, c.keys.Previous):
144			// Move to the previous input
145			c.inputs[c.focused].Blur()
146			c.focused = (c.focused - 1 + len(c.inputs)) % len(c.inputs)
147			c.inputs[c.focused].Focus()
148		case key.Matches(msg, c.keys.Paste):
149			return c, textinput.Paste
150		case key.Matches(msg, c.keys.Close):
151			return c, util.CmdHandler(dialogs.CloseDialogMsg{})
152		default:
153			var cmd tea.Cmd
154			c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
155			return c, cmd
156		}
157	case tea.PasteMsg:
158		var cmd tea.Cmd
159		c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
160		return c, cmd
161	}
162	return c, nil
163}
164
165// View implements CommandArgumentsDialog.
166func (c *commandArgumentsDialogCmp) View() string {
167	t := styles.CurrentTheme()
168	baseStyle := t.S().Base
169
170	title := lipgloss.NewStyle().
171		Foreground(t.Primary).
172		Bold(true).
173		Padding(0, 1).
174		Render(cmp.Or(c.title, c.name))
175
176	promptName := t.S().Text.
177		Padding(0, 1).
178		Render(c.description)
179
180	inputFields := make([]string, len(c.inputs))
181	for i, input := range c.inputs {
182		labelStyle := baseStyle.Padding(1, 1, 0, 1)
183
184		if i == c.focused {
185			labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
186		} else {
187			labelStyle = labelStyle.Foreground(t.FgMuted)
188		}
189
190		arg := c.arguments[i]
191		argName := cmp.Or(arg.Title, arg.Name)
192		if arg.Required {
193			argName += "*"
194		}
195		label := labelStyle.Render(argName + ":")
196
197		field := t.S().Text.
198			Padding(0, 1).
199			Render(input.View())
200
201		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
202	}
203
204	elements := []string{title, promptName}
205	elements = append(elements, inputFields...)
206
207	c.help.ShowAll = false
208	helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
209	elements = append(elements, "", helpText)
210
211	content := lipgloss.JoinVertical(lipgloss.Left, elements...)
212
213	return baseStyle.Padding(1, 1, 0, 1).
214		Border(lipgloss.RoundedBorder()).
215		BorderForeground(t.BorderFocus).
216		Width(c.width).
217		Render(content)
218}
219
220func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
221	if len(c.inputs) == 0 {
222		return nil
223	}
224	cursor := c.inputs[c.focused].Cursor()
225	if cursor != nil {
226		cursor = c.moveCursor(cursor)
227	}
228	return cursor
229}
230
231const (
232	headerHeight      = 3
233	itemHeight        = 3
234	paddingHorizontal = 3
235)
236
237func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
238	row, col := c.Position()
239	offset := row + headerHeight + (1+c.focused)*itemHeight
240	cursor.Y += offset
241	cursor.X = cursor.X + col + paddingHorizontal
242	return cursor
243}
244
245func (c *commandArgumentsDialogCmp) Position() (int, int) {
246	row := (c.wHeight / 2) - (c.height / 2)
247	col := (c.wWidth / 2) - (c.width / 2)
248	return row, col
249}
250
251// ID implements CommandArgumentsDialog.
252func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID {
253	return argumentsDialogID
254}