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) (util.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.Close):
132 return c, util.CmdHandler(dialogs.CloseDialogMsg{})
133 default:
134 var cmd tea.Cmd
135 c.inputs[c.focusIndex], cmd = c.inputs[c.focusIndex].Update(msg)
136 return c, cmd
137 }
138 case tea.PasteMsg:
139 var cmd tea.Cmd
140 c.inputs[c.focusIndex], cmd = c.inputs[c.focusIndex].Update(msg)
141 return c, cmd
142 }
143 return c, nil
144}
145
146// View implements CommandArgumentsDialog.
147func (c *commandArgumentsDialogCmp) View() string {
148 t := styles.CurrentTheme()
149 baseStyle := t.S().Base
150
151 title := lipgloss.NewStyle().
152 Foreground(t.Primary).
153 Bold(true).
154 Padding(0, 1).
155 Render("Command Arguments")
156
157 explanation := t.S().Text.
158 Padding(0, 1).
159 Render("This command requires arguments.")
160
161 // Create input fields for each argument
162 inputFields := make([]string, len(c.inputs))
163 for i, input := range c.inputs {
164 // Highlight the label of the focused input
165 labelStyle := baseStyle.
166 Padding(1, 1, 0, 1)
167
168 if i == c.focusIndex {
169 labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
170 } else {
171 labelStyle = labelStyle.Foreground(t.FgMuted)
172 }
173
174 label := labelStyle.Render(c.argNames[i] + ":")
175
176 field := t.S().Text.
177 Padding(0, 1).
178 Render(input.View())
179
180 inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
181 }
182
183 // Join all elements vertically
184 elements := []string{title, explanation}
185 elements = append(elements, inputFields...)
186
187 c.help.ShowAll = false
188 helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
189 elements = append(elements, "", helpText)
190
191 content := lipgloss.JoinVertical(
192 lipgloss.Left,
193 elements...,
194 )
195
196 return baseStyle.Padding(1, 1, 0, 1).
197 Border(lipgloss.RoundedBorder()).
198 BorderForeground(t.BorderFocus).
199 Width(c.width).
200 Render(content)
201}
202
203func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
204 cursor := c.inputs[c.focusIndex].Cursor()
205 if cursor != nil {
206 cursor = c.moveCursor(cursor)
207 }
208 return cursor
209}
210
211func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
212 row, col := c.Position()
213 offset := row + 3 + (1+c.focusIndex)*3
214 cursor.Y += offset
215 cursor.X = cursor.X + col + 3
216 return cursor
217}
218
219func (c *commandArgumentsDialogCmp) Position() (int, int) {
220 row := c.wHeight / 2
221 row -= c.wHeight / 2
222 col := c.wWidth / 2
223 col -= c.width / 2
224 return row, col
225}
226
227// ID implements CommandArgumentsDialog.
228func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID {
229 return argumentsDialogID
230}