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.`
184
185 // Mark the project as initialized
186 if err := config.MarkProjectInitialized(); err != nil {
187 return util.ReportError(err)
188 }
189
190 return tea.Sequence(
191 util.CmdHandler(cmpChat.SessionClearedMsg{}),
192 util.CmdHandler(cmpChat.SendMsg{
193 Text: prompt,
194 }),
195 )
196 } else {
197 // Mark the project as initialized without running the command
198 if err := config.MarkProjectInitialized(); err != nil {
199 return util.ReportError(err)
200 }
201 }
202 return nil
203}
204
205// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
206type CloseInitDialogMsg struct {
207 Initialize bool
208}
209
210// ShowInitDialogMsg is a message that is sent to show the init dialog.
211type ShowInitDialogMsg struct {
212 Show bool
213}