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