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}