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