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/config"
  9	cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
 10	"github.com/charmbracelet/crush/internal/tui/components/core"
 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)
 15
 16const InitDialogID dialogs.DialogID = "init"
 17
 18// InitDialogCmp is a component that asks the user if they want to initialize the project.
 19type InitDialogCmp interface {
 20	dialogs.DialogModel
 21}
 22
 23type initDialogCmp struct {
 24	wWidth, wHeight int
 25	width, height   int
 26	selected        int
 27	keyMap          KeyMap
 28}
 29
 30// NewInitDialogCmp creates a new InitDialogCmp.
 31func NewInitDialogCmp() InitDialogCmp {
 32	return &initDialogCmp{
 33		selected: 0,
 34		keyMap:   DefaultKeyMap(),
 35	}
 36}
 37
 38// Init implements tea.Model.
 39func (m *initDialogCmp) Init() tea.Cmd {
 40	return nil
 41}
 42
 43// Update implements tea.Model.
 44func (m *initDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 45	switch msg := msg.(type) {
 46	case tea.WindowSizeMsg:
 47		m.wWidth = msg.Width
 48		m.wHeight = msg.Height
 49		cmd := m.SetSize()
 50		return m, cmd
 51	case tea.KeyPressMsg:
 52		switch {
 53		case key.Matches(msg, m.keyMap.Close):
 54			return m, tea.Batch(
 55				util.CmdHandler(dialogs.CloseDialogMsg{}),
 56				m.handleInitialization(false),
 57			)
 58		case key.Matches(msg, m.keyMap.ChangeSelection):
 59			m.selected = (m.selected + 1) % 2
 60			return m, nil
 61		case key.Matches(msg, m.keyMap.Select):
 62			return m, tea.Batch(
 63				util.CmdHandler(dialogs.CloseDialogMsg{}),
 64				m.handleInitialization(m.selected == 0),
 65			)
 66		case key.Matches(msg, m.keyMap.Y):
 67			return m, tea.Batch(
 68				util.CmdHandler(dialogs.CloseDialogMsg{}),
 69				m.handleInitialization(true),
 70			)
 71		case key.Matches(msg, m.keyMap.N):
 72			return m, tea.Batch(
 73				util.CmdHandler(dialogs.CloseDialogMsg{}),
 74				m.handleInitialization(false),
 75			)
 76		}
 77	}
 78	return m, nil
 79}
 80
 81func (m *initDialogCmp) renderButtons() string {
 82	t := styles.CurrentTheme()
 83	baseStyle := t.S().Base
 84
 85	buttons := []core.ButtonOpts{
 86		{
 87			Text:           "Yes",
 88			UnderlineIndex: 0, // "Y"
 89			Selected:       m.selected == 0,
 90		},
 91		{
 92			Text:           "No",
 93			UnderlineIndex: 0, // "N"
 94			Selected:       m.selected == 1,
 95		},
 96	}
 97
 98	content := core.SelectableButtons(buttons, "  ")
 99
100	return baseStyle.AlignHorizontal(lipgloss.Right).Width(m.width - 4).Render(content)
101}
102
103func (m *initDialogCmp) renderContent() string {
104	t := styles.CurrentTheme()
105	baseStyle := t.S().Base
106
107	explanation := t.S().Text.
108		Width(m.width - 4).
109		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.")
110
111	question := t.S().Text.
112		Width(m.width - 4).
113		Render("Would you like to initialize this project?")
114
115	return baseStyle.Render(lipgloss.JoinVertical(
116		lipgloss.Left,
117		explanation,
118		"",
119		question,
120	))
121}
122
123func (m *initDialogCmp) render() string {
124	t := styles.CurrentTheme()
125	baseStyle := t.S().Base
126	title := core.Title("Initialize Project", m.width-4)
127
128	content := m.renderContent()
129	buttons := m.renderButtons()
130
131	dialogContent := lipgloss.JoinVertical(
132		lipgloss.Top,
133		title,
134		"",
135		content,
136		"",
137		buttons,
138		"",
139	)
140
141	return baseStyle.
142		Padding(0, 1).
143		Border(lipgloss.RoundedBorder()).
144		BorderForeground(t.BorderFocus).
145		Width(m.width).
146		Render(dialogContent)
147}
148
149// View implements tea.Model.
150func (m *initDialogCmp) View() tea.View {
151	return tea.NewView(m.render())
152}
153
154// SetSize sets the size of the component.
155func (m *initDialogCmp) SetSize() tea.Cmd {
156	m.width = min(90, m.wWidth)
157	m.height = min(15, m.wHeight)
158	return nil
159}
160
161// ID implements DialogModel.
162func (m *initDialogCmp) ID() dialogs.DialogID {
163	return InitDialogID
164}
165
166// Position implements DialogModel.
167func (m *initDialogCmp) Position() (int, int) {
168	row := (m.wHeight / 2) - (m.height / 2)
169	col := (m.wWidth / 2) - (m.width / 2)
170	return row, col
171}
172
173// handleInitialization handles the initialization logic when the dialog is closed.
174func (m *initDialogCmp) handleInitialization(initialize bool) tea.Cmd {
175	if initialize {
176		// Run the initialization command
177		prompt := `Please analyze this codebase and create a CRUSH.md file containing:
1781. Build/lint/test commands - especially for running a single test
1792. Code style guidelines including imports, formatting, types, naming conventions, error handling, etc.
180
181The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
182If there's already a CRUSH.md, improve it.
183If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
184Add the .crush directory to the .gitignore file if it's not already there.`
185
186		// Mark the project as initialized
187		if err := config.MarkProjectInitialized(); err != nil {
188			return util.ReportError(err)
189		}
190
191		return tea.Sequence(
192			util.CmdHandler(cmpChat.SessionClearedMsg{}),
193			util.CmdHandler(cmpChat.SendMsg{
194				Text: prompt,
195			}),
196		)
197	} else {
198		// Mark the project as initialized without running the command
199		if err := config.MarkProjectInitialized(); err != nil {
200			return util.ReportError(err)
201		}
202	}
203	return nil
204}
205
206// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
207type CloseInitDialogMsg struct {
208	Initialize bool
209}
210
211// ShowInitDialogMsg is a message that is sent to show the init dialog.
212type ShowInitDialogMsg struct {
213	Show bool
214}