arguments.go

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