init.go

  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/opencode-ai/opencode/internal/tui/styles"
  9	"github.com/opencode-ai/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}