1package dialog
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/styles"
9 "github.com/charmbracelet/crush/internal/tui/util"
10)
11
12// InitDialogCmp is a component that asks the user if they want to initialize the project.
13type InitDialogCmp struct {
14 width, height int
15 selected int
16 keys initDialogKeyMap
17}
18
19// NewInitDialogCmp creates a new InitDialogCmp.
20func NewInitDialogCmp() InitDialogCmp {
21 return InitDialogCmp{
22 selected: 0,
23 keys: initDialogKeyMap{},
24 }
25}
26
27type initDialogKeyMap struct {
28 Tab key.Binding
29 Left key.Binding
30 Right key.Binding
31 Enter key.Binding
32 Escape key.Binding
33 Y key.Binding
34 N key.Binding
35}
36
37// ShortHelp implements key.Map.
38func (k initDialogKeyMap) ShortHelp() []key.Binding {
39 return []key.Binding{
40 key.NewBinding(
41 key.WithKeys("tab", "left", "right"),
42 key.WithHelp("tab/←/→", "toggle selection"),
43 ),
44 key.NewBinding(
45 key.WithKeys("enter"),
46 key.WithHelp("enter", "confirm"),
47 ),
48 key.NewBinding(
49 key.WithKeys("esc", "q"),
50 key.WithHelp("esc/q", "cancel"),
51 ),
52 key.NewBinding(
53 key.WithKeys("y", "n"),
54 key.WithHelp("y/n", "yes/no"),
55 ),
56 }
57}
58
59// FullHelp implements key.Map.
60func (k initDialogKeyMap) FullHelp() [][]key.Binding {
61 return [][]key.Binding{k.ShortHelp()}
62}
63
64// Init implements tea.Model.
65func (m InitDialogCmp) Init() tea.Cmd {
66 return nil
67}
68
69// Update implements tea.Model.
70func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
71 switch msg := msg.(type) {
72 case tea.KeyPressMsg:
73 switch {
74 case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
75 return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
76 case key.Matches(msg, key.NewBinding(key.WithKeys("tab", "left", "right", "h", "l"))):
77 m.selected = (m.selected + 1) % 2
78 return m, nil
79 case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
80 return m, util.CmdHandler(CloseInitDialogMsg{Initialize: m.selected == 0})
81 case key.Matches(msg, key.NewBinding(key.WithKeys("y"))):
82 return m, util.CmdHandler(CloseInitDialogMsg{Initialize: true})
83 case key.Matches(msg, key.NewBinding(key.WithKeys("n"))):
84 return m, util.CmdHandler(CloseInitDialogMsg{Initialize: false})
85 }
86 case tea.WindowSizeMsg:
87 m.width = msg.Width
88 m.height = msg.Height
89 }
90 return m, nil
91}
92
93// View implements tea.Model.
94func (m InitDialogCmp) View() string {
95 t := styles.CurrentTheme()
96 baseStyle := t.S().Base
97
98 // Calculate width needed for content
99 maxWidth := 60 // Width for explanation text
100
101 title := baseStyle.
102 Foreground(t.Primary).
103 Bold(true).
104 Width(maxWidth).
105 Padding(0, 1).
106 Render("Initialize Project")
107
108 explanation := t.S().Text.
109 Width(maxWidth).
110 Padding(0, 1).
111 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.")
112
113 question := t.S().Text.
114 Width(maxWidth).
115 Padding(1, 1).
116 Render("Would you like to initialize this project?")
117
118 maxWidth = min(maxWidth, m.width-10)
119 yesStyle := t.S().Text
120 noStyle := yesStyle
121
122 if m.selected == 0 {
123 yesStyle = yesStyle.
124 Background(t.Primary).
125 Bold(true)
126 noStyle = noStyle.
127 Background(t.BgSubtle)
128 } else {
129 noStyle = noStyle.
130 Background(t.Primary).
131 Bold(true)
132 yesStyle = yesStyle.
133 Background(t.BgSubtle)
134 }
135
136 yes := yesStyle.Padding(0, 3).Render("Yes")
137 no := noStyle.Padding(0, 3).Render("No")
138
139 buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render(" "), no)
140 buttons = baseStyle.
141 Width(maxWidth).
142 Padding(1, 0).
143 Render(buttons)
144
145 content := lipgloss.JoinVertical(
146 lipgloss.Left,
147 title,
148 baseStyle.Width(maxWidth).Render(""),
149 explanation,
150 question,
151 buttons,
152 baseStyle.Width(maxWidth).Render(""),
153 )
154
155 return baseStyle.Padding(1, 2).
156 Border(lipgloss.RoundedBorder()).
157 BorderForeground(t.BorderFocus).
158 Width(lipgloss.Width(content) + 4).
159 Render(content)
160}
161
162// SetSize sets the size of the component.
163func (m *InitDialogCmp) SetSize(width, height int) {
164 m.width = width
165 m.height = height
166}
167
168// Bindings implements layout.Bindings.
169func (m InitDialogCmp) Bindings() []key.Binding {
170 return m.keys.ShortHelp()
171}
172
173// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
174type CloseInitDialogMsg struct {
175 Initialize bool
176}
177
178// ShowInitDialogMsg is a message that is sent to show the init dialog.
179type ShowInitDialogMsg struct {
180 Show bool
181}