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