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", "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.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 maxWidth = min(maxWidth, m.width-10)
118 yesStyle := styles.BaseStyle
119 noStyle := styles.BaseStyle
120
121 if m.selected == 0 {
122 yesStyle = yesStyle.
123 Background(styles.PrimaryColor).
124 Foreground(styles.Background).
125 Bold(true)
126 noStyle = noStyle.
127 Background(styles.Background).
128 Foreground(styles.PrimaryColor)
129 } else {
130 noStyle = noStyle.
131 Background(styles.PrimaryColor).
132 Foreground(styles.Background).
133 Bold(true)
134 yesStyle = yesStyle.
135 Background(styles.Background).
136 Foreground(styles.PrimaryColor)
137 }
138
139 yes := yesStyle.Padding(0, 3).Render("Yes")
140 no := noStyle.Padding(0, 3).Render("No")
141
142 buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, styles.BaseStyle.Render(" "), no)
143 buttons = styles.BaseStyle.
144 Width(maxWidth).
145 Padding(1, 0).
146 Render(buttons)
147
148 content := lipgloss.JoinVertical(
149 lipgloss.Left,
150 title,
151 styles.BaseStyle.Width(maxWidth).Render(""),
152 explanation,
153 question,
154 buttons,
155 styles.BaseStyle.Width(maxWidth).Render(""),
156 )
157
158 return styles.BaseStyle.Padding(1, 2).
159 Border(lipgloss.RoundedBorder()).
160 BorderBackground(styles.Background).
161 BorderForeground(styles.ForgroundDim).
162 Width(lipgloss.Width(content) + 4).
163 Render(content)
164}
165
166// SetSize sets the size of the component.
167func (m *InitDialogCmp) SetSize(width, height int) {
168 m.width = width
169 m.height = height
170}
171
172// Bindings implements layout.Bindings.
173func (m InitDialogCmp) Bindings() []key.Binding {
174 return m.keys.ShortHelp()
175}
176
177// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
178type CloseInitDialogMsg struct {
179 Initialize bool
180}
181
182// ShowInitDialogMsg is a message that is sent to show the init dialog.
183type ShowInitDialogMsg struct {
184 Show bool
185}