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.Name)
 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		argName := c.arguments[i].Name
186		if c.arguments[i].Required {
187			argName += " *"
188		}
189		label := labelStyle.Render(argName + ":")
190
191		field := t.S().Text.
192			Padding(0, 1).
193			Render(input.View())
194
195		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
196	}
197
198	elements := []string{title, promptName}
199	elements = append(elements, inputFields...)
200
201	c.help.ShowAll = false
202	helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
203	elements = append(elements, "", helpText)
204
205	content := lipgloss.JoinVertical(lipgloss.Left, elements...)
206
207	return baseStyle.Padding(1, 1, 0, 1).
208		Border(lipgloss.RoundedBorder()).
209		BorderForeground(t.BorderFocus).
210		Width(c.width).
211		Render(content)
212}
213
214func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
215	if len(c.inputs) == 0 {
216		return nil
217	}
218	cursor := c.inputs[c.focused].Cursor()
219	if cursor != nil {
220		cursor = c.moveCursor(cursor)
221	}
222	return cursor
223}
224
225const (
226	headerHeight      = 3
227	itemHeight        = 3
228	paddingHorizontal = 3
229)
230
231func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
232	row, col := c.Position()
233	offset := row + headerHeight + (1+c.focused)*itemHeight
234	cursor.Y += offset
235	cursor.X = cursor.X + col + paddingHorizontal
236	return cursor
237}
238
239func (c *commandArgumentsDialogCmp) Position() (int, int) {
240	row := (c.wHeight / 2) - (c.height / 2)
241	col := (c.wWidth / 2) - (c.width / 2)
242	return row, col
243}
244
245// ID implements CommandArgumentsDialog.
246func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID {
247	return argumentsDialogID
248}