1package commands
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	"github.com/charmbracelet/bubbles/v2/help"
  8	"github.com/charmbracelet/bubbles/v2/key"
  9	"github.com/charmbracelet/bubbles/v2/textinput"
 10	tea "github.com/charmbracelet/bubbletea/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/lipgloss/v2"
 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 struct {
 23	CommandID 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	width   int
 43	wWidth  int // Width of the terminal window
 44	wHeight int // Height of the terminal window
 45
 46	inputs     []textinput.Model
 47	focusIndex int
 48	keys       ArgumentsDialogKeyMap
 49	commandID  string
 50	content    string
 51	argNames   []string
 52	help       help.Model
 53}
 54
 55func NewCommandArgumentsDialog(commandID, content string, argNames []string) CommandArgumentsDialog {
 56	t := styles.CurrentTheme()
 57	inputs := make([]textinput.Model, len(argNames))
 58
 59	for i, name := range argNames {
 60		ti := textinput.New()
 61		ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
 62		ti.SetWidth(40)
 63		ti.SetVirtualCursor(false)
 64		ti.Prompt = ""
 65
 66		ti.SetStyles(t.S().TextInput)
 67		// Only focus the first input initially
 68		if i == 0 {
 69			ti.Focus()
 70		} else {
 71			ti.Blur()
 72		}
 73
 74		inputs[i] = ti
 75	}
 76
 77	return &commandArgumentsDialogCmp{
 78		inputs:     inputs,
 79		keys:       DefaultArgumentsDialogKeyMap(),
 80		commandID:  commandID,
 81		content:    content,
 82		argNames:   argNames,
 83		focusIndex: 0,
 84		width:      60,
 85		help:       help.New(),
 86	}
 87}
 88
 89// Init implements CommandArgumentsDialog.
 90func (c *commandArgumentsDialogCmp) Init() tea.Cmd {
 91	return nil
 92}
 93
 94// Update implements CommandArgumentsDialog.
 95func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
 96	switch msg := msg.(type) {
 97	case tea.WindowSizeMsg:
 98		c.wWidth = msg.Width
 99		c.wHeight = msg.Height
100	case tea.KeyPressMsg:
101		switch {
102		case key.Matches(msg, c.keys.Confirm):
103			if c.focusIndex == len(c.inputs)-1 {
104				content := c.content
105				for i, name := range c.argNames {
106					value := c.inputs[i].Value()
107					placeholder := "$" + name
108					content = strings.ReplaceAll(content, placeholder, value)
109				}
110				return c, tea.Sequence(
111					util.CmdHandler(dialogs.CloseDialogMsg{}),
112					util.CmdHandler(CommandRunCustomMsg{
113						Content: content,
114					}),
115				)
116			}
117			// Otherwise, move to the next input
118			c.inputs[c.focusIndex].Blur()
119			c.focusIndex++
120			c.inputs[c.focusIndex].Focus()
121		case key.Matches(msg, c.keys.Next):
122			// Move to the next input
123			c.inputs[c.focusIndex].Blur()
124			c.focusIndex = (c.focusIndex + 1) % len(c.inputs)
125			c.inputs[c.focusIndex].Focus()
126		case key.Matches(msg, c.keys.Previous):
127			// Move to the previous input
128			c.inputs[c.focusIndex].Blur()
129			c.focusIndex = (c.focusIndex - 1 + len(c.inputs)) % len(c.inputs)
130			c.inputs[c.focusIndex].Focus()
131		case key.Matches(msg, c.keys.Close):
132			return c, util.CmdHandler(dialogs.CloseDialogMsg{})
133		default:
134			var cmd tea.Cmd
135			c.inputs[c.focusIndex], cmd = c.inputs[c.focusIndex].Update(msg)
136			return c, cmd
137		}
138	case tea.PasteMsg:
139		var cmd tea.Cmd
140		c.inputs[c.focusIndex], cmd = c.inputs[c.focusIndex].Update(msg)
141		return c, cmd
142	}
143	return c, nil
144}
145
146// View implements CommandArgumentsDialog.
147func (c *commandArgumentsDialogCmp) View() string {
148	t := styles.CurrentTheme()
149	baseStyle := t.S().Base
150
151	title := lipgloss.NewStyle().
152		Foreground(t.Primary).
153		Bold(true).
154		Padding(0, 1).
155		Render("Command Arguments")
156
157	explanation := t.S().Text.
158		Padding(0, 1).
159		Render("This command requires arguments.")
160
161	// Create input fields for each argument
162	inputFields := make([]string, len(c.inputs))
163	for i, input := range c.inputs {
164		// Highlight the label of the focused input
165		labelStyle := baseStyle.
166			Padding(1, 1, 0, 1)
167
168		if i == c.focusIndex {
169			labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
170		} else {
171			labelStyle = labelStyle.Foreground(t.FgMuted)
172		}
173
174		label := labelStyle.Render(c.argNames[i] + ":")
175
176		field := t.S().Text.
177			Padding(0, 1).
178			Render(input.View())
179
180		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
181	}
182
183	// Join all elements vertically
184	elements := []string{title, explanation}
185	elements = append(elements, inputFields...)
186
187	c.help.ShowAll = false
188	helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
189	elements = append(elements, "", helpText)
190
191	content := lipgloss.JoinVertical(
192		lipgloss.Left,
193		elements...,
194	)
195
196	return baseStyle.Padding(1, 1, 0, 1).
197		Border(lipgloss.RoundedBorder()).
198		BorderForeground(t.BorderFocus).
199		Width(c.width).
200		Render(content)
201}
202
203func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
204	cursor := c.inputs[c.focusIndex].Cursor()
205	if cursor != nil {
206		cursor = c.moveCursor(cursor)
207	}
208	return cursor
209}
210
211func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
212	row, col := c.Position()
213	offset := row + 3 + (1+c.focusIndex)*3
214	cursor.Y += offset
215	cursor.X = cursor.X + col + 3
216	return cursor
217}
218
219func (c *commandArgumentsDialogCmp) Position() (int, int) {
220	row := c.wHeight / 2
221	row -= c.wHeight / 2
222	col := c.wWidth / 2
223	col -= c.width / 2
224	return row, col
225}
226
227// ID implements CommandArgumentsDialog.
228func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID {
229	return argumentsDialogID
230}