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() string {
151 return 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}