1package dialog
2
3import (
4 "github.com/charmbracelet/bubbles/key"
5 tea "github.com/charmbracelet/bubbletea"
6 "github.com/charmbracelet/lipgloss"
7
8 "github.com/kujtimiihoxha/opencode/internal/tui/styles"
9 "github.com/kujtimiihoxha/opencode/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"),
50 key.WithHelp("esc", "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.KeyMsg:
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 // Calculate width needed for content
96 maxWidth := 60 // Width for explanation text
97
98 title := styles.BaseStyle.
99 Foreground(styles.PrimaryColor).
100 Bold(true).
101 Width(maxWidth).
102 Padding(0, 1).
103 Render("Initialize Project")
104
105 explanation := styles.BaseStyle.
106 Foreground(styles.Forground).
107 Width(maxWidth).
108 Padding(0, 1).
109 Render("Initialization generates a new OpenCode.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 := styles.BaseStyle.
112 Foreground(styles.Forground).
113 Width(maxWidth).
114 Padding(1, 1).
115 Render("Would you like to initialize this project?")
116
117 yesStyle := styles.BaseStyle
118 noStyle := styles.BaseStyle
119
120 if m.selected == 0 {
121 yesStyle = yesStyle.
122 Background(styles.PrimaryColor).
123 Foreground(styles.Background).
124 Bold(true)
125 noStyle = noStyle.
126 Background(styles.Background).
127 Foreground(styles.PrimaryColor)
128 } else {
129 noStyle = noStyle.
130 Background(styles.PrimaryColor).
131 Foreground(styles.Background).
132 Bold(true)
133 yesStyle = yesStyle.
134 Background(styles.Background).
135 Foreground(styles.PrimaryColor)
136 }
137
138 yes := yesStyle.Padding(0, 3).Render("Yes")
139 no := noStyle.Padding(0, 3).Render("No")
140
141 buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, styles.BaseStyle.Render(" "), no)
142 buttons = styles.BaseStyle.
143 Width(maxWidth).
144 Padding(1, 0).
145 Render(buttons)
146
147 help := styles.BaseStyle.
148 Width(maxWidth).
149 Padding(0, 1).
150 Foreground(styles.ForgroundDim).
151 Render("tab/←/→: toggle y/n: yes/no enter: confirm esc: cancel")
152
153 content := lipgloss.JoinVertical(
154 lipgloss.Left,
155 title,
156 styles.BaseStyle.Width(maxWidth).Render(""),
157 explanation,
158 question,
159 buttons,
160 styles.BaseStyle.Width(maxWidth).Render(""),
161 help,
162 )
163
164 return styles.BaseStyle.Padding(1, 2).
165 Border(lipgloss.RoundedBorder()).
166 BorderBackground(styles.Background).
167 BorderForeground(styles.ForgroundDim).
168 Width(lipgloss.Width(content) + 4).
169 Render(content)
170}
171
172// SetSize sets the size of the component.
173func (m *InitDialogCmp) SetSize(width, height int) {
174 m.width = width
175 m.height = height
176}
177
178// Bindings implements layout.Bindings.
179func (m InitDialogCmp) Bindings() []key.Binding {
180 return m.keys.ShortHelp()
181}
182
183// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
184type CloseInitDialogMsg struct {
185 Initialize bool
186}
187
188// ShowInitDialogMsg is a message that is sent to show the init dialog.
189type ShowInitDialogMsg struct {
190 Show bool
191}