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/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}