init.go

  1package init
  2
  3import (
  4	"github.com/charmbracelet/bubbles/v2/key"
  5	tea "github.com/charmbracelet/bubbletea/v2"
  6	"github.com/charmbracelet/lipgloss/v2"
  7
  8	"github.com/charmbracelet/crush/internal/tui/components/core"
  9	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 10	"github.com/charmbracelet/crush/internal/tui/styles"
 11	"github.com/charmbracelet/crush/internal/tui/util"
 12)
 13
 14const InitDialogID dialogs.DialogID = "init"
 15
 16// InitDialogCmp is a component that asks the user if they want to initialize the project.
 17type InitDialogCmp interface {
 18	dialogs.DialogModel
 19}
 20
 21type initDialogCmp struct {
 22	wWidth, wHeight int
 23	width, height   int
 24	selected        int
 25	keyMap          KeyMap
 26}
 27
 28// NewInitDialogCmp creates a new InitDialogCmp.
 29func NewInitDialogCmp() InitDialogCmp {
 30	return &initDialogCmp{
 31		selected: 0,
 32		keyMap:   DefaultKeyMap(),
 33	}
 34}
 35
 36// Init implements tea.Model.
 37func (m *initDialogCmp) Init() tea.Cmd {
 38	return nil
 39}
 40
 41// Update implements tea.Model.
 42func (m *initDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 43	switch msg := msg.(type) {
 44	case tea.WindowSizeMsg:
 45		m.wWidth = msg.Width
 46		m.wHeight = msg.Height
 47		cmd := m.SetSize()
 48		return m, cmd
 49	case tea.KeyPressMsg:
 50		switch {
 51		case key.Matches(msg, m.keyMap.Close):
 52			return m, tea.Batch(
 53				util.CmdHandler(dialogs.CloseDialogMsg{}),
 54				util.CmdHandler(CloseInitDialogMsg{Initialize: false}),
 55			)
 56		case key.Matches(msg, m.keyMap.ChangeSelection):
 57			m.selected = (m.selected + 1) % 2
 58			return m, nil
 59		case key.Matches(msg, m.keyMap.Select):
 60			return m, tea.Batch(
 61				util.CmdHandler(dialogs.CloseDialogMsg{}),
 62				util.CmdHandler(CloseInitDialogMsg{Initialize: m.selected == 0}),
 63			)
 64		case key.Matches(msg, m.keyMap.Y):
 65			return m, tea.Batch(
 66				util.CmdHandler(dialogs.CloseDialogMsg{}),
 67				util.CmdHandler(CloseInitDialogMsg{Initialize: true}),
 68			)
 69		case key.Matches(msg, m.keyMap.N):
 70			return m, tea.Batch(
 71				util.CmdHandler(dialogs.CloseDialogMsg{}),
 72				util.CmdHandler(CloseInitDialogMsg{Initialize: false}),
 73			)
 74		}
 75	}
 76	return m, nil
 77}
 78
 79func (m *initDialogCmp) renderButtons() string {
 80	t := styles.CurrentTheme()
 81	baseStyle := t.S().Base
 82
 83	buttons := []core.ButtonOpts{
 84		{
 85			Text:           "Yes",
 86			UnderlineIndex: 0, // "Y"
 87			Selected:       m.selected == 0,
 88		},
 89		{
 90			Text:           "No",
 91			UnderlineIndex: 0, // "N"
 92			Selected:       m.selected == 1,
 93		},
 94	}
 95
 96	content := core.SelectableButtons(buttons, "  ")
 97
 98	return baseStyle.AlignHorizontal(lipgloss.Right).Width(m.width - 4).Render(content)
 99}
100
101func (m *initDialogCmp) renderContent() string {
102	t := styles.CurrentTheme()
103	baseStyle := t.S().Base
104
105	explanation := t.S().Text.
106		Width(m.width - 4).
107		Render("Initialization generates a new Crush.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
108
109	question := t.S().Text.
110		Width(m.width - 4).
111		Render("Would you like to initialize this project?")
112
113	return baseStyle.Render(lipgloss.JoinVertical(
114		lipgloss.Left,
115		explanation,
116		"",
117		question,
118	))
119}
120
121func (m *initDialogCmp) render() string {
122	t := styles.CurrentTheme()
123	baseStyle := t.S().Base
124	title := core.Title("Initialize Project", m.width-4)
125
126	content := m.renderContent()
127	buttons := m.renderButtons()
128
129	dialogContent := lipgloss.JoinVertical(
130		lipgloss.Top,
131		title,
132		"",
133		content,
134		"",
135		buttons,
136		"",
137	)
138
139	return baseStyle.
140		Padding(0, 1).
141		Border(lipgloss.RoundedBorder()).
142		BorderForeground(t.BorderFocus).
143		Width(m.width).
144		Render(dialogContent)
145}
146
147// View implements tea.Model.
148func (m *initDialogCmp) View() tea.View {
149	return tea.NewView(m.render())
150}
151
152// SetSize sets the size of the component.
153func (m *initDialogCmp) SetSize() tea.Cmd {
154	m.width = min(90, m.wWidth)
155	m.height = min(15, m.wHeight)
156	return nil
157}
158
159// ID implements DialogModel.
160func (m *initDialogCmp) ID() dialogs.DialogID {
161	return InitDialogID
162}
163
164// Position implements DialogModel.
165func (m *initDialogCmp) Position() (int, int) {
166	row := (m.wHeight / 2) - (m.height / 2)
167	col := (m.wWidth / 2) - (m.width / 2)
168	return row, col
169}
170
171// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
172type CloseInitDialogMsg struct {
173	Initialize bool
174}
175
176// ShowInitDialogMsg is a message that is sent to show the init dialog.
177type ShowInitDialogMsg struct {
178	Show bool
179}