1package commands
2
3import (
4 "cmp"
5
6 "charm.land/bubbles/v2/help"
7 "charm.land/bubbles/v2/key"
8 "charm.land/bubbles/v2/textinput"
9 tea "charm.land/bubbletea/v2"
10 "charm.land/lipgloss/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/crush/internal/uicmd"
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 = uicmd.ShowArgumentsDialogMsg
23
24// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
25type CloseArgumentsDialogMsg = uicmd.CloseArgumentsDialogMsg
26
27// CommandArgumentsDialog represents the commands dialog.
28type CommandArgumentsDialog interface {
29 dialogs.DialogModel
30}
31
32type commandArgumentsDialogCmp struct {
33 wWidth, wHeight int
34 width, height int
35
36 inputs []textinput.Model
37 focused int
38 keys ArgumentsDialogKeyMap
39 arguments []Argument
40 help help.Model
41
42 id string
43 title string
44 name string
45 description string
46
47 onSubmit func(args map[string]string) tea.Cmd
48}
49
50type Argument struct {
51 Name, Title, Description string
52 Required bool
53}
54
55func NewCommandArgumentsDialog(
56 id, title, name, description string,
57 arguments []Argument,
58 onSubmit func(args map[string]string) tea.Cmd,
59) CommandArgumentsDialog {
60 t := styles.CurrentTheme()
61 inputs := make([]textinput.Model, len(arguments))
62
63 for i, arg := range arguments {
64 ti := textinput.New()
65 ti.Placeholder = cmp.Or(arg.Description, "Enter value for "+arg.Title)
66 ti.SetWidth(40)
67 ti.SetVirtualCursor(false)
68 ti.Prompt = ""
69
70 ti.SetStyles(t.S().TextInput)
71 // Only focus the first input initially
72 if i == 0 {
73 ti.Focus()
74 } else {
75 ti.Blur()
76 }
77
78 inputs[i] = ti
79 }
80
81 return &commandArgumentsDialogCmp{
82 inputs: inputs,
83 keys: DefaultArgumentsDialogKeyMap(),
84 id: id,
85 name: name,
86 title: title,
87 description: description,
88 arguments: arguments,
89 width: 60,
90 help: help.New(),
91 onSubmit: onSubmit,
92 }
93}
94
95// Init implements CommandArgumentsDialog.
96func (c *commandArgumentsDialogCmp) Init() tea.Cmd {
97 return nil
98}
99
100// Update implements CommandArgumentsDialog.
101func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
102 switch msg := msg.(type) {
103 case tea.WindowSizeMsg:
104 c.wWidth = msg.Width
105 c.wHeight = msg.Height
106 c.width = min(90, c.wWidth)
107 c.height = min(15, c.wHeight)
108 for i := range c.inputs {
109 c.inputs[i].SetWidth(c.width - (paddingHorizontal * 2))
110 }
111 case tea.KeyPressMsg:
112 switch {
113 case key.Matches(msg, c.keys.Close):
114 return c, util.CmdHandler(dialogs.CloseDialogMsg{})
115 case key.Matches(msg, c.keys.Confirm):
116 if c.focused == len(c.inputs)-1 {
117 args := make(map[string]string)
118 for i, arg := range c.arguments {
119 value := c.inputs[i].Value()
120 args[arg.Name] = value
121 }
122 return c, tea.Sequence(
123 util.CmdHandler(dialogs.CloseDialogMsg{}),
124 c.onSubmit(args),
125 )
126 }
127 // Otherwise, move to the next input
128 c.inputs[c.focused].Blur()
129 c.focused++
130 c.inputs[c.focused].Focus()
131 case key.Matches(msg, c.keys.Next):
132 // Move to the next input
133 c.inputs[c.focused].Blur()
134 c.focused = (c.focused + 1) % len(c.inputs)
135 c.inputs[c.focused].Focus()
136 case key.Matches(msg, c.keys.Previous):
137 // Move to the previous input
138 c.inputs[c.focused].Blur()
139 c.focused = (c.focused - 1 + len(c.inputs)) % len(c.inputs)
140 c.inputs[c.focused].Focus()
141 case key.Matches(msg, c.keys.Close):
142 return c, util.CmdHandler(dialogs.CloseDialogMsg{})
143 default:
144 var cmd tea.Cmd
145 c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
146 return c, cmd
147 }
148 case tea.PasteMsg:
149 var cmd tea.Cmd
150 c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
151 return c, cmd
152 }
153 return c, nil
154}
155
156// View implements CommandArgumentsDialog.
157func (c *commandArgumentsDialogCmp) View() string {
158 t := styles.CurrentTheme()
159 baseStyle := t.S().Base
160
161 title := lipgloss.NewStyle().
162 Foreground(t.Primary).
163 Bold(true).
164 Padding(0, 1).
165 Render(cmp.Or(c.title, c.name))
166
167 promptName := t.S().Text.
168 Padding(0, 1).
169 Render(c.description)
170
171 inputFields := make([]string, len(c.inputs))
172 for i, input := range c.inputs {
173 labelStyle := baseStyle.Padding(1, 1, 0, 1)
174
175 if i == c.focused {
176 labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
177 } else {
178 labelStyle = labelStyle.Foreground(t.FgMuted)
179 }
180
181 arg := c.arguments[i]
182 argName := cmp.Or(arg.Title, arg.Name)
183 if arg.Required {
184 argName += "*"
185 }
186 label := labelStyle.Render(argName + ":")
187
188 field := t.S().Text.
189 Padding(0, 1).
190 Render(input.View())
191
192 inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
193 }
194
195 elements := []string{title, promptName}
196 elements = append(elements, inputFields...)
197
198 c.help.ShowAll = false
199 helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
200 elements = append(elements, "", helpText)
201
202 content := lipgloss.JoinVertical(lipgloss.Left, elements...)
203
204 return baseStyle.Padding(1, 1, 0, 1).
205 Border(lipgloss.RoundedBorder()).
206 BorderForeground(t.BorderFocus).
207 Width(c.width).
208 Render(content)
209}
210
211func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
212 if len(c.inputs) == 0 {
213 return nil
214 }
215 cursor := c.inputs[c.focused].Cursor()
216 if cursor != nil {
217 cursor = c.moveCursor(cursor)
218 }
219 return cursor
220}
221
222const (
223 headerHeight = 3
224 itemHeight = 3
225 paddingHorizontal = 3
226)
227
228func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
229 row, col := c.Position()
230 offset := row + headerHeight + (1+c.focused)*itemHeight
231 cursor.Y += offset
232 cursor.X = cursor.X + col + paddingHorizontal
233 return cursor
234}
235
236func (c *commandArgumentsDialogCmp) Position() (int, int) {
237 row := (c.wHeight / 2) - (c.height / 2)
238 col := (c.wWidth / 2) - (c.width / 2)
239 return row, col
240}
241
242// ID implements CommandArgumentsDialog.
243func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID {
244 return argumentsDialogID
245}