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}