arguments.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. package dialog
  2. import (
  3. "fmt"
  4. "github.com/charmbracelet/bubbles/key"
  5. "github.com/charmbracelet/bubbles/textinput"
  6. tea "github.com/charmbracelet/bubbletea"
  7. "github.com/charmbracelet/lipgloss"
  8. "github.com/sst/opencode/internal/styles"
  9. "github.com/sst/opencode/internal/theme"
  10. "github.com/sst/opencode/internal/util"
  11. )
  12. type argumentsDialogKeyMap struct {
  13. Enter key.Binding
  14. Escape key.Binding
  15. }
  16. // ShortHelp implements key.Map.
  17. func (k argumentsDialogKeyMap) ShortHelp() []key.Binding {
  18. return []key.Binding{
  19. key.NewBinding(
  20. key.WithKeys("enter"),
  21. key.WithHelp("enter", "confirm"),
  22. ),
  23. key.NewBinding(
  24. key.WithKeys("esc"),
  25. key.WithHelp("esc", "cancel"),
  26. ),
  27. }
  28. }
  29. // FullHelp implements key.Map.
  30. func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
  31. return [][]key.Binding{k.ShortHelp()}
  32. }
  33. // ShowMultiArgumentsDialogMsg is a message that is sent to show the multi-arguments dialog.
  34. type ShowMultiArgumentsDialogMsg struct {
  35. CommandID string
  36. Content string
  37. ArgNames []string
  38. }
  39. // CloseMultiArgumentsDialogMsg is a message that is sent when the multi-arguments dialog is closed.
  40. type CloseMultiArgumentsDialogMsg struct {
  41. Submit bool
  42. CommandID string
  43. Content string
  44. Args map[string]string
  45. }
  46. // MultiArgumentsDialogCmp is a component that asks the user for multiple command arguments.
  47. type MultiArgumentsDialogCmp struct {
  48. width, height int
  49. inputs []textinput.Model
  50. focusIndex int
  51. keys argumentsDialogKeyMap
  52. commandID string
  53. content string
  54. argNames []string
  55. }
  56. // NewMultiArgumentsDialogCmp creates a new MultiArgumentsDialogCmp.
  57. func NewMultiArgumentsDialogCmp(commandID, content string, argNames []string) MultiArgumentsDialogCmp {
  58. t := theme.CurrentTheme()
  59. inputs := make([]textinput.Model, len(argNames))
  60. for i, name := range argNames {
  61. ti := textinput.New()
  62. ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
  63. ti.Width = 40
  64. ti.Prompt = ""
  65. ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
  66. ti.PromptStyle = ti.PromptStyle.Background(t.Background())
  67. ti.TextStyle = ti.TextStyle.Background(t.Background())
  68. // Only focus the first input initially
  69. if i == 0 {
  70. ti.Focus()
  71. ti.PromptStyle = ti.PromptStyle.Foreground(t.Primary())
  72. ti.TextStyle = ti.TextStyle.Foreground(t.Primary())
  73. } else {
  74. ti.Blur()
  75. }
  76. inputs[i] = ti
  77. }
  78. return MultiArgumentsDialogCmp{
  79. inputs: inputs,
  80. keys: argumentsDialogKeyMap{},
  81. commandID: commandID,
  82. content: content,
  83. argNames: argNames,
  84. focusIndex: 0,
  85. }
  86. }
  87. // Init implements tea.Model.
  88. func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
  89. // Make sure only the first input is focused
  90. for i := range m.inputs {
  91. if i == 0 {
  92. m.inputs[i].Focus()
  93. } else {
  94. m.inputs[i].Blur()
  95. }
  96. }
  97. return textinput.Blink
  98. }
  99. // Update implements tea.Model.
  100. func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  101. var cmds []tea.Cmd
  102. t := theme.CurrentTheme()
  103. switch msg := msg.(type) {
  104. case tea.KeyMsg:
  105. switch {
  106. case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
  107. return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
  108. Submit: false,
  109. CommandID: m.commandID,
  110. Content: m.content,
  111. Args: nil,
  112. })
  113. case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
  114. // If we're on the last input, submit the form
  115. if m.focusIndex == len(m.inputs)-1 {
  116. args := make(map[string]string)
  117. for i, name := range m.argNames {
  118. args[name] = m.inputs[i].Value()
  119. }
  120. return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
  121. Submit: true,
  122. CommandID: m.commandID,
  123. Content: m.content,
  124. Args: args,
  125. })
  126. }
  127. // Otherwise, move to the next input
  128. m.inputs[m.focusIndex].Blur()
  129. m.focusIndex++
  130. m.inputs[m.focusIndex].Focus()
  131. m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
  132. m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
  133. case key.Matches(msg, key.NewBinding(key.WithKeys("tab"))):
  134. // Move to the next input
  135. m.inputs[m.focusIndex].Blur()
  136. m.focusIndex = (m.focusIndex + 1) % len(m.inputs)
  137. m.inputs[m.focusIndex].Focus()
  138. m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
  139. m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
  140. case key.Matches(msg, key.NewBinding(key.WithKeys("shift+tab"))):
  141. // Move to the previous input
  142. m.inputs[m.focusIndex].Blur()
  143. m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs)
  144. m.inputs[m.focusIndex].Focus()
  145. m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
  146. m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
  147. }
  148. case tea.WindowSizeMsg:
  149. m.width = msg.Width
  150. m.height = msg.Height
  151. }
  152. // Update the focused input
  153. var cmd tea.Cmd
  154. m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
  155. cmds = append(cmds, cmd)
  156. return m, tea.Batch(cmds...)
  157. }
  158. // View implements tea.Model.
  159. func (m MultiArgumentsDialogCmp) View() string {
  160. t := theme.CurrentTheme()
  161. baseStyle := styles.BaseStyle()
  162. // Calculate width needed for content
  163. maxWidth := 60 // Width for explanation text
  164. title := lipgloss.NewStyle().
  165. Foreground(t.Primary()).
  166. Bold(true).
  167. Width(maxWidth).
  168. Padding(0, 1).
  169. Background(t.Background()).
  170. Render("Command Arguments")
  171. explanation := lipgloss.NewStyle().
  172. Foreground(t.Text()).
  173. Width(maxWidth).
  174. Padding(0, 1).
  175. Background(t.Background()).
  176. Render("This command requires multiple arguments. Please enter values for each:")
  177. // Create input fields for each argument
  178. inputFields := make([]string, len(m.inputs))
  179. for i, input := range m.inputs {
  180. // Highlight the label of the focused input
  181. labelStyle := lipgloss.NewStyle().
  182. Width(maxWidth).
  183. Padding(1, 1, 0, 1).
  184. Background(t.Background())
  185. if i == m.focusIndex {
  186. labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
  187. } else {
  188. labelStyle = labelStyle.Foreground(t.TextMuted())
  189. }
  190. label := labelStyle.Render(m.argNames[i] + ":")
  191. field := lipgloss.NewStyle().
  192. Foreground(t.Text()).
  193. Width(maxWidth).
  194. Padding(0, 1).
  195. Background(t.Background()).
  196. Render(input.View())
  197. inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
  198. }
  199. maxWidth = min(maxWidth, m.width-10)
  200. // Join all elements vertically
  201. elements := []string{title, explanation}
  202. elements = append(elements, inputFields...)
  203. content := lipgloss.JoinVertical(
  204. lipgloss.Left,
  205. elements...,
  206. )
  207. return baseStyle.Padding(1, 2).
  208. Border(lipgloss.RoundedBorder()).
  209. BorderBackground(t.Background()).
  210. BorderForeground(t.TextMuted()).
  211. Background(t.Background()).
  212. Width(lipgloss.Width(content) + 4).
  213. Render(content)
  214. }
  215. // SetSize sets the size of the component.
  216. func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
  217. m.width = width
  218. m.height = height
  219. }
  220. // Bindings implements layout.Bindings.
  221. func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
  222. return m.keys.ShortHelp()
  223. }