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