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) (tea.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
132		default:
133			var cmd tea.Cmd
134			c.inputs[c.focusIndex], cmd = c.inputs[c.focusIndex].Update(msg)
135			return c, cmd
136		}
137	}
138	return c, nil
139}
140
141// View implements CommandArgumentsDialog.
142func (c *commandArgumentsDialogCmp) View() string {
143	t := styles.CurrentTheme()
144	baseStyle := t.S().Base
145
146	title := lipgloss.NewStyle().
147		Foreground(t.Primary).
148		Bold(true).
149		Padding(0, 1).
150		Render("Command Arguments")
151
152	explanation := t.S().Text.
153		Padding(0, 1).
154		Render("This command requires arguments.")
155
156	// Create input fields for each argument
157	inputFields := make([]string, len(c.inputs))
158	for i, input := range c.inputs {
159		// Highlight the label of the focused input
160		labelStyle := baseStyle.
161			Padding(1, 1, 0, 1)
162
163		if i == c.focusIndex {
164			labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
165		} else {
166			labelStyle = labelStyle.Foreground(t.FgMuted)
167		}
168
169		label := labelStyle.Render(c.argNames[i] + ":")
170
171		field := t.S().Text.
172			Padding(0, 1).
173			Render(input.View())
174
175		inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
176	}
177
178	// Join all elements vertically
179	elements := []string{title, explanation}
180	elements = append(elements, inputFields...)
181
182	c.help.ShowAll = false
183	helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
184	elements = append(elements, "", helpText)
185
186	content := lipgloss.JoinVertical(
187		lipgloss.Left,
188		elements...,
189	)
190
191	return baseStyle.Padding(1, 1, 0, 1).
192		Border(lipgloss.RoundedBorder()).
193		BorderForeground(t.BorderFocus).
194		Width(c.width).
195		Render(content)
196}
197
198func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
199	cursor := c.inputs[c.focusIndex].Cursor()
200	if cursor != nil {
201		cursor = c.moveCursor(cursor)
202	}
203	return cursor
204}
205
206func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
207	row, col := c.Position()
208	offset := row + 3 + (1+c.focusIndex)*3
209	cursor.Y += offset
210	cursor.X = cursor.X + col + 3
211	return cursor
212}
213
214func (c *commandArgumentsDialogCmp) Position() (int, int) {
215	row := c.wHeight / 2
216	row -= c.wHeight / 2
217	col := c.wWidth / 2
218	col -= c.width / 2
219	return row, col
220}
221
222// ID implements CommandArgumentsDialog.
223func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID {
224	return argumentsDialogID
225}