arguments.go 6.2 KB

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