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