arguments.go

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