| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- package commands
- import (
- "cmp"
- "github.com/charmbracelet/bubbles/v2/help"
- "github.com/charmbracelet/bubbles/v2/key"
- "github.com/charmbracelet/bubbles/v2/textinput"
- tea "github.com/charmbracelet/bubbletea/v2"
- "github.com/charmbracelet/crush/internal/tui/components/dialogs"
- "github.com/charmbracelet/crush/internal/tui/styles"
- "github.com/charmbracelet/crush/internal/tui/util"
- "github.com/charmbracelet/lipgloss/v2"
- )
- const (
- argumentsDialogID dialogs.DialogID = "arguments"
- )
- // ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
- type ShowArgumentsDialogMsg struct {
- CommandID string
- Description string
- ArgNames []string
- OnSubmit func(args map[string]string) tea.Cmd
- }
- // CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
- type CloseArgumentsDialogMsg struct {
- Submit bool
- CommandID string
- Content string
- Args map[string]string
- }
- // CommandArgumentsDialog represents the commands dialog.
- type CommandArgumentsDialog interface {
- dialogs.DialogModel
- }
- type commandArgumentsDialogCmp struct {
- wWidth, wHeight int
- width, height int
- inputs []textinput.Model
- focused int
- keys ArgumentsDialogKeyMap
- arguments []Argument
- help help.Model
- id string
- title string
- name string
- description string
- onSubmit func(args map[string]string) tea.Cmd
- }
- type Argument struct {
- Name, Title, Description string
- Required bool
- }
- func NewCommandArgumentsDialog(
- id, title, name, description string,
- arguments []Argument,
- onSubmit func(args map[string]string) tea.Cmd,
- ) CommandArgumentsDialog {
- t := styles.CurrentTheme()
- inputs := make([]textinput.Model, len(arguments))
- for i, arg := range arguments {
- ti := textinput.New()
- ti.Placeholder = cmp.Or(arg.Description, "Enter value for "+arg.Title)
- ti.SetWidth(40)
- ti.SetVirtualCursor(false)
- ti.Prompt = ""
- ti.SetStyles(t.S().TextInput)
- // Only focus the first input initially
- if i == 0 {
- ti.Focus()
- } else {
- ti.Blur()
- }
- inputs[i] = ti
- }
- return &commandArgumentsDialogCmp{
- inputs: inputs,
- keys: DefaultArgumentsDialogKeyMap(),
- id: id,
- name: name,
- title: title,
- description: description,
- arguments: arguments,
- width: 60,
- help: help.New(),
- onSubmit: onSubmit,
- }
- }
- // Init implements CommandArgumentsDialog.
- func (c *commandArgumentsDialogCmp) Init() tea.Cmd {
- return nil
- }
- // Update implements CommandArgumentsDialog.
- func (c *commandArgumentsDialogCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) {
- switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- c.wWidth = msg.Width
- c.wHeight = msg.Height
- c.width = min(90, c.wWidth)
- c.height = min(15, c.wHeight)
- for i := range c.inputs {
- c.inputs[i].SetWidth(c.width - (paddingHorizontal * 2))
- }
- case tea.KeyPressMsg:
- switch {
- case key.Matches(msg, c.keys.Close):
- return c, util.CmdHandler(dialogs.CloseDialogMsg{})
- case key.Matches(msg, c.keys.Confirm):
- if c.focused == len(c.inputs)-1 {
- args := make(map[string]string)
- for i, arg := range c.arguments {
- value := c.inputs[i].Value()
- args[arg.Name] = value
- }
- return c, tea.Sequence(
- util.CmdHandler(dialogs.CloseDialogMsg{}),
- c.onSubmit(args),
- )
- }
- // Otherwise, move to the next input
- c.inputs[c.focused].Blur()
- c.focused++
- c.inputs[c.focused].Focus()
- case key.Matches(msg, c.keys.Next):
- // Move to the next input
- c.inputs[c.focused].Blur()
- c.focused = (c.focused + 1) % len(c.inputs)
- c.inputs[c.focused].Focus()
- case key.Matches(msg, c.keys.Previous):
- // Move to the previous input
- c.inputs[c.focused].Blur()
- c.focused = (c.focused - 1 + len(c.inputs)) % len(c.inputs)
- c.inputs[c.focused].Focus()
- case key.Matches(msg, c.keys.Close):
- return c, util.CmdHandler(dialogs.CloseDialogMsg{})
- default:
- var cmd tea.Cmd
- c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
- return c, cmd
- }
- case tea.PasteMsg:
- var cmd tea.Cmd
- c.inputs[c.focused], cmd = c.inputs[c.focused].Update(msg)
- return c, cmd
- }
- return c, nil
- }
- // View implements CommandArgumentsDialog.
- func (c *commandArgumentsDialogCmp) View() string {
- t := styles.CurrentTheme()
- baseStyle := t.S().Base
- title := lipgloss.NewStyle().
- Foreground(t.Primary).
- Bold(true).
- Padding(0, 1).
- Render(cmp.Or(c.title, c.name))
- promptName := t.S().Text.
- Padding(0, 1).
- Render(c.description)
- inputFields := make([]string, len(c.inputs))
- for i, input := range c.inputs {
- labelStyle := baseStyle.Padding(1, 1, 0, 1)
- if i == c.focused {
- labelStyle = labelStyle.Foreground(t.FgBase).Bold(true)
- } else {
- labelStyle = labelStyle.Foreground(t.FgMuted)
- }
- arg := c.arguments[i]
- argName := cmp.Or(arg.Title, arg.Name)
- if arg.Required {
- argName += "*"
- }
- label := labelStyle.Render(argName + ":")
- field := t.S().Text.
- Padding(0, 1).
- Render(input.View())
- inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
- }
- elements := []string{title, promptName}
- elements = append(elements, inputFields...)
- c.help.ShowAll = false
- helpText := baseStyle.Padding(0, 1).Render(c.help.View(c.keys))
- elements = append(elements, "", helpText)
- content := lipgloss.JoinVertical(lipgloss.Left, elements...)
- return baseStyle.Padding(1, 1, 0, 1).
- Border(lipgloss.RoundedBorder()).
- BorderForeground(t.BorderFocus).
- Width(c.width).
- Render(content)
- }
- func (c *commandArgumentsDialogCmp) Cursor() *tea.Cursor {
- if len(c.inputs) == 0 {
- return nil
- }
- cursor := c.inputs[c.focused].Cursor()
- if cursor != nil {
- cursor = c.moveCursor(cursor)
- }
- return cursor
- }
- const (
- headerHeight = 3
- itemHeight = 3
- paddingHorizontal = 3
- )
- func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor {
- row, col := c.Position()
- offset := row + headerHeight + (1+c.focused)*itemHeight
- cursor.Y += offset
- cursor.X = cursor.X + col + paddingHorizontal
- return cursor
- }
- func (c *commandArgumentsDialogCmp) Position() (int, int) {
- row := (c.wHeight / 2) - (c.height / 2)
- col := (c.wWidth / 2) - (c.width / 2)
- return row, col
- }
- // ID implements CommandArgumentsDialog.
- func (c *commandArgumentsDialogCmp) ID() dialogs.DialogID {
- return argumentsDialogID
- }
|